frameworc-mcp 0.2.0 → 0.3.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 +215 -215
- package/dist/api-client.js +73 -26
- package/dist/catalogue.js +5 -5
- package/dist/index.js +149 -29
- package/dist/session.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,216 +1,216 @@
|
|
|
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
|
|
10
|
-
- Resolves referenced `Form` / `Menu` / `Prefill` entries via dedicated list tools
|
|
11
|
-
- 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
|
-
- Draft by default — created pages have `is_enabled = false`; the human flips the switch in the OctCMS backend after assigning images
|
|
13
|
-
|
|
14
|
-
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.
|
|
15
|
-
|
|
16
|
-
## Architecture
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
chat client (Claude Desktop / opencode / Cursor / Cline / Continue / ...)
|
|
20
|
-
│ stdio JSON-RPC
|
|
21
|
-
▼
|
|
22
|
-
node dist/index.js (local, spawned by the chat client)
|
|
23
|
-
reads ~/.config/frameworc/sites.json (auto-created on first run; hot-reloaded)
|
|
24
|
-
│ HTTPS + Authorization: Bearer <token resolved by URL>
|
|
25
|
-
▼
|
|
26
|
-
any OctCMS install /api/mcp/v1/* (provided by the FrameworC plugin v1.10.0+)
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
No Docker, no ToolHive, no remote gateway, no second auth layer. Each OctCMS install has its own bearer token stored in its own `FrameworcSetting → Integration → MCP API token`, and your laptop keeps the URL → token map in one local file.
|
|
30
|
-
|
|
31
|
-
## Install
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
git clone <your-gitlab-url>/frameworc-mcp.git
|
|
35
|
-
cd frameworc-mcp
|
|
36
|
-
npm install
|
|
37
|
-
npm run build
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
(Optional) publish to your GitLab group's npm registry so colleagues can `npx` it without a clone — see [Publishing](#publishing) below.
|
|
41
|
-
|
|
42
|
-
## Claude Desktop config
|
|
43
|
-
|
|
44
|
-
Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`; Linux: `~/.config/Claude/`):
|
|
45
|
-
|
|
46
|
-
```json
|
|
47
|
-
{
|
|
48
|
-
"mcpServers": {
|
|
49
|
-
"frameworc": {
|
|
50
|
-
"command": "npx",
|
|
51
|
-
"args": ["-y", "@yourorg/frameworc-mcp"]
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
If you cloned instead of `npx`-installed, point `args` to your local `dist/index.js`:
|
|
58
|
-
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"mcpServers": {
|
|
62
|
-
"frameworc": {
|
|
63
|
-
"command": "node",
|
|
64
|
-
"args": ["/absolute/path/to/frameworc-mcp/dist/index.js"]
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
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).
|
|
71
|
-
|
|
72
|
-
## Per-site token file — `~/.config/frameworc/sites.json`
|
|
73
|
-
|
|
74
|
-
The MCP stores one bearer token per OctCMS install in this file on your laptop. It's the only credential store. Format:
|
|
75
|
-
|
|
76
|
-
```json
|
|
77
|
-
{
|
|
78
|
-
"sites": [
|
|
79
|
-
{ "label": "Client A prod", "url": "https://clienta.test", "token": "AAA..." },
|
|
80
|
-
{ "label": "Client A staging", "url": "https://staging.clienta.test", "token": "BBB..." },
|
|
81
|
-
{ "label": "Myco", "url": "https://myco.example", "token": "CCC..." }
|
|
82
|
-
]
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
- `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".
|
|
87
|
-
- `url` is matched after normalising trailing slashes — `https://x.test/` and `https://x.test` are the same.
|
|
88
|
-
- `token` is sent as `Authorization: Bearer <token>` on every HTTP call to that site. Never committed to git.
|
|
89
|
-
|
|
90
|
-
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.
|
|
91
|
-
|
|
92
|
-
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.
|
|
93
|
-
|
|
94
|
-
### Adding a new OctCMS site
|
|
95
|
-
|
|
96
|
-
1. Spin up the new OctCMS install with the FrameworC plugin (v1.10.0 or newer).
|
|
97
|
-
2. Backend → Settings → FrameworC → Integration → **MCP API token** → paste a freshly generated random string (e.g. `openssl rand -hex 32`) → Save.
|
|
98
|
-
3. Edit `~/.config/frameworc/sites.json` on your laptop, add one entry: `{ "label": "New Client", "url": "https://newsite.test", "token": "<that string>" }`.
|
|
99
|
-
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
|
-
|
|
101
|
-
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.
|
|
102
|
-
|
|
103
|
-
### Security
|
|
104
|
-
|
|
105
|
-
- The file should be `0600` (auto-set on first creation). If you `chmod` it looser, the MCP prints a stderr warning on startup.
|
|
106
|
-
- 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
|
-
- 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
|
-
|
|
109
|
-
## Tools (v1)
|
|
110
|
-
|
|
111
|
-
| Tool | Description |
|
|
112
|
-
|---|---|
|
|
113
|
-
| `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
|
-
| `list_sites` | List configured sites (label + URL only, no tokens) from `sites.json`. |
|
|
115
|
-
| `list_pages` | List all pages on the pinned site. |
|
|
116
|
-
| `get_page(id)` | Full page JSON (page meta + builder blocks + nested repeaters). |
|
|
117
|
-
| `create_page(payload)` | Create a draft page. `payload = { page: {...}, builder: [...] }`. |
|
|
118
|
-
| `update_page(id, payload)` | Edit page meta, or full-rebuild the builder array. |
|
|
119
|
-
| `delete_page(id)` | Soft-delete a page. |
|
|
120
|
-
| `add_block(page_id, block, position?)` | Append (or insert at `position`) a block. |
|
|
121
|
-
| `update_block(page_id, block_id, block)` | Replace one block by row id. |
|
|
122
|
-
| `remove_block(page_id, block_id)` | Remove one block by row id. |
|
|
123
|
-
| `reorder_blocks(page_id, order)` | Reorder blocks (order = array of all row ids in new order). |
|
|
124
|
-
| `list_forms` | Form Tailor entries for the `Form` block's `form_entry`. |
|
|
125
|
-
| `list_menus` | Menu Tailor entries for the `MenuBlock`'s `content.menu_entry`. |
|
|
126
|
-
| `list_prefills` | Prefill Tailor entries for the `Prefill` block's `content.prefill_entry`. |
|
|
127
|
-
| `get_block_schema(name)` | Field schema + usage notes for one block. |
|
|
128
|
-
|
|
129
|
-
## Resource
|
|
130
|
-
|
|
131
|
-
- `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.
|
|
132
|
-
|
|
133
|
-
## Page-content JSON shape
|
|
134
|
-
|
|
135
|
-
`create_page` / `update_page` `payload`:
|
|
136
|
-
|
|
137
|
-
```json
|
|
138
|
-
{
|
|
139
|
-
"page": {
|
|
140
|
-
"title": "Contact",
|
|
141
|
-
"slug": "contact",
|
|
142
|
-
"fullslug": "contact",
|
|
143
|
-
"is_enabled": false,
|
|
144
|
-
"metaTitle": "",
|
|
145
|
-
"metaDescription": "",
|
|
146
|
-
"menuStyle": "solid",
|
|
147
|
-
"menuHide": "no"
|
|
148
|
-
},
|
|
149
|
-
"builder": [
|
|
150
|
-
{
|
|
151
|
-
"content_group": "Header",
|
|
152
|
-
"base": {
|
|
153
|
-
"blockId": "kontakt",
|
|
154
|
-
"headline": "<h1>Contact us</h1>",
|
|
155
|
-
"elevated": false,
|
|
156
|
-
"containerWidth": "default",
|
|
157
|
-
"backgroundColor": "default",
|
|
158
|
-
"customCssClass": [],
|
|
159
|
-
"responsiveHide": []
|
|
160
|
-
},
|
|
161
|
-
"content": {
|
|
162
|
-
"image": "",
|
|
163
|
-
"imageMobile": "",
|
|
164
|
-
"isVideoBg": false,
|
|
165
|
-
"buttonLabel1": "",
|
|
166
|
-
"buttonLink1": "",
|
|
167
|
-
"buttonBlank1": false,
|
|
168
|
-
"fullHeight": true,
|
|
169
|
-
"contrast": false,
|
|
170
|
-
"overlay": false
|
|
171
|
-
}
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
"content_group": "Form",
|
|
175
|
-
"base": { "blockId": "formular", "headline": "<h2>Write us</h2>" },
|
|
176
|
-
"form_entry": 7,
|
|
177
|
-
"content": { "variant": "default" }
|
|
178
|
-
}
|
|
179
|
-
]
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### Storage encoding (what the MCP understands)
|
|
184
|
-
|
|
185
|
-
- Media fields (`image`, `imageMobile`, `backgroundImage`, `buttonIcon1..4`, `ogImage`, `images`, `file`) **must be empty** in the JSON — they are rejected if non-empty. Fill them in the OctCMS backend.
|
|
186
|
-
- `switch` fields accept booleans; the API normalises to `"1"` / `"0"` for storage.
|
|
187
|
-
- `customCssClass` + `responsiveHide` are arrays of strings.
|
|
188
|
-
- Multi-`mediafinder` fields (`images` for `Gallery` / `ImageStrip`) are arrays of path strings — but must be empty `[]` per the rule above.
|
|
189
|
-
- `entries` links are integers: `form_entry` (block level for `Form`), `content.menu_entry` (for `MenuBlock`), `content.prefill_entry` (for `Prefill`).
|
|
190
|
-
- `Slider` accepts an optional `content.breakpoints = { tablet: number, mobile: number }`.
|
|
191
|
-
- `Columns` blocks have `content.columns = [{ blockId, builder: [...blocks] }]` — recursive: each column's `builder` follows the exact same shape as the top-level `builder`.
|
|
192
|
-
|
|
193
|
-
## Catalogue drift
|
|
194
|
-
|
|
195
|
-
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.
|
|
196
|
-
|
|
197
|
-
## Publishing
|
|
198
|
-
|
|
199
|
-
To publish to your GitLab group's npm registry:
|
|
200
|
-
|
|
201
|
-
1. Edit `package.json`:
|
|
202
|
-
- Replace `@yourorg` in `name` and `publishConfig["@yourorg:registry"]` with your actual GitLab scope/group.
|
|
203
|
-
- Replace `<YOUR-PROJECT-ID>` in `publishConfig["@yourorg:registry"]` with the numeric project id of the `frameworc-mcp` GitLab repo.
|
|
204
|
-
2. Create a GitLab deploy token / project access token with `api` + `write_registry` scope. Add a `.npmrc` at the registry host:
|
|
205
|
-
|
|
206
|
-
```
|
|
207
|
-
@yourorg:registry=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/
|
|
208
|
-
//gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/:_authToken=<TOKEN>
|
|
209
|
-
```
|
|
210
|
-
3. `npm publish`.
|
|
211
|
-
|
|
212
|
-
Colleagues then use `npx -y @yourorg/frameworc-mcp` in their `claude_desktop_config.json` (no clone needed).
|
|
213
|
-
|
|
214
|
-
## License
|
|
215
|
-
|
|
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
|
|
10
|
+
- Resolves referenced `Form` / `Menu` / `Prefill` entries via dedicated list tools
|
|
11
|
+
- 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
|
+
- Draft by default — created pages have `is_enabled = false`; the human flips the switch in the OctCMS backend after assigning images
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
chat client (Claude Desktop / opencode / Cursor / Cline / Continue / ...)
|
|
20
|
+
│ stdio JSON-RPC
|
|
21
|
+
▼
|
|
22
|
+
node dist/index.js (local, spawned by the chat client)
|
|
23
|
+
reads ~/.config/frameworc/sites.json (auto-created on first run; hot-reloaded)
|
|
24
|
+
│ HTTPS + Authorization: Bearer <token resolved by URL>
|
|
25
|
+
▼
|
|
26
|
+
any OctCMS install /api/mcp/v1/* (provided by the FrameworC plugin v1.10.0+)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
No Docker, no ToolHive, no remote gateway, no second auth layer. Each OctCMS install has its own bearer token stored in its own `FrameworcSetting → Integration → MCP API token`, and your laptop keeps the URL → token map in one local file.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone <your-gitlab-url>/frameworc-mcp.git
|
|
35
|
+
cd frameworc-mcp
|
|
36
|
+
npm install
|
|
37
|
+
npm run build
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
(Optional) publish to your GitLab group's npm registry so colleagues can `npx` it without a clone — see [Publishing](#publishing) below.
|
|
41
|
+
|
|
42
|
+
## Claude Desktop config
|
|
43
|
+
|
|
44
|
+
Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`; Linux: `~/.config/Claude/`):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"frameworc": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "@yourorg/frameworc-mcp"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If you cloned instead of `npx`-installed, point `args` to your local `dist/index.js`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"frameworc": {
|
|
63
|
+
"command": "node",
|
|
64
|
+
"args": ["/absolute/path/to/frameworc-mcp/dist/index.js"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
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).
|
|
71
|
+
|
|
72
|
+
## Per-site token file — `~/.config/frameworc/sites.json`
|
|
73
|
+
|
|
74
|
+
The MCP stores one bearer token per OctCMS install in this file on your laptop. It's the only credential store. Format:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"sites": [
|
|
79
|
+
{ "label": "Client A prod", "url": "https://clienta.test", "token": "AAA..." },
|
|
80
|
+
{ "label": "Client A staging", "url": "https://staging.clienta.test", "token": "BBB..." },
|
|
81
|
+
{ "label": "Myco", "url": "https://myco.example", "token": "CCC..." }
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `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".
|
|
87
|
+
- `url` is matched after normalising trailing slashes — `https://x.test/` and `https://x.test` are the same.
|
|
88
|
+
- `token` is sent as `Authorization: Bearer <token>` on every HTTP call to that site. Never committed to git.
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
### Adding a new OctCMS site
|
|
95
|
+
|
|
96
|
+
1. Spin up the new OctCMS install with the FrameworC plugin (v1.10.0 or newer).
|
|
97
|
+
2. Backend → Settings → FrameworC → Integration → **MCP API token** → paste a freshly generated random string (e.g. `openssl rand -hex 32`) → Save.
|
|
98
|
+
3. Edit `~/.config/frameworc/sites.json` on your laptop, add one entry: `{ "label": "New Client", "url": "https://newsite.test", "token": "<that string>" }`.
|
|
99
|
+
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
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
### Security
|
|
104
|
+
|
|
105
|
+
- The file should be `0600` (auto-set on first creation). If you `chmod` it looser, the MCP prints a stderr warning on startup.
|
|
106
|
+
- 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
|
+
- 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
|
+
|
|
109
|
+
## Tools (v1)
|
|
110
|
+
|
|
111
|
+
| Tool | Description |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `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
|
+
| `list_sites` | List configured sites (label + URL only, no tokens) from `sites.json`. |
|
|
115
|
+
| `list_pages` | List all pages on the pinned site. |
|
|
116
|
+
| `get_page(id)` | Full page JSON (page meta + builder blocks + nested repeaters). |
|
|
117
|
+
| `create_page(payload)` | Create a draft page. `payload = { page: {...}, builder: [...] }`. |
|
|
118
|
+
| `update_page(id, payload)` | Edit page meta, or full-rebuild the builder array. |
|
|
119
|
+
| `delete_page(id)` | Soft-delete a page. |
|
|
120
|
+
| `add_block(page_id, block, position?)` | Append (or insert at `position`) a block. |
|
|
121
|
+
| `update_block(page_id, block_id, block)` | Replace one block by row id. |
|
|
122
|
+
| `remove_block(page_id, block_id)` | Remove one block by row id. |
|
|
123
|
+
| `reorder_blocks(page_id, order)` | Reorder blocks (order = array of all row ids in new order). |
|
|
124
|
+
| `list_forms` | Form Tailor entries for the `Form` block's `form_entry`. |
|
|
125
|
+
| `list_menus` | Menu Tailor entries for the `MenuBlock`'s `content.menu_entry`. |
|
|
126
|
+
| `list_prefills` | Prefill Tailor entries for the `Prefill` block's `content.prefill_entry`. |
|
|
127
|
+
| `get_block_schema(name)` | Field schema + usage notes for one block. |
|
|
128
|
+
|
|
129
|
+
## Resource
|
|
130
|
+
|
|
131
|
+
- `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.
|
|
132
|
+
|
|
133
|
+
## Page-content JSON shape
|
|
134
|
+
|
|
135
|
+
`create_page` / `update_page` `payload`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"page": {
|
|
140
|
+
"title": "Contact",
|
|
141
|
+
"slug": "contact",
|
|
142
|
+
"fullslug": "contact",
|
|
143
|
+
"is_enabled": false,
|
|
144
|
+
"metaTitle": "",
|
|
145
|
+
"metaDescription": "",
|
|
146
|
+
"menuStyle": "solid",
|
|
147
|
+
"menuHide": "no"
|
|
148
|
+
},
|
|
149
|
+
"builder": [
|
|
150
|
+
{
|
|
151
|
+
"content_group": "Header",
|
|
152
|
+
"base": {
|
|
153
|
+
"blockId": "kontakt",
|
|
154
|
+
"headline": "<h1>Contact us</h1>",
|
|
155
|
+
"elevated": false,
|
|
156
|
+
"containerWidth": "default",
|
|
157
|
+
"backgroundColor": "default",
|
|
158
|
+
"customCssClass": [],
|
|
159
|
+
"responsiveHide": []
|
|
160
|
+
},
|
|
161
|
+
"content": {
|
|
162
|
+
"image": "",
|
|
163
|
+
"imageMobile": "",
|
|
164
|
+
"isVideoBg": false,
|
|
165
|
+
"buttonLabel1": "",
|
|
166
|
+
"buttonLink1": "",
|
|
167
|
+
"buttonBlank1": false,
|
|
168
|
+
"fullHeight": true,
|
|
169
|
+
"contrast": false,
|
|
170
|
+
"overlay": false
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"content_group": "Form",
|
|
175
|
+
"base": { "blockId": "formular", "headline": "<h2>Write us</h2>" },
|
|
176
|
+
"form_entry": 7,
|
|
177
|
+
"content": { "variant": "default" }
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Storage encoding (what the MCP understands)
|
|
184
|
+
|
|
185
|
+
- Media fields (`image`, `imageMobile`, `backgroundImage`, `buttonIcon1..4`, `ogImage`, `images`, `file`) **must be empty** in the JSON — they are rejected if non-empty. Fill them in the OctCMS backend.
|
|
186
|
+
- `switch` fields accept booleans; the API normalises to `"1"` / `"0"` for storage.
|
|
187
|
+
- `customCssClass` + `responsiveHide` are arrays of strings.
|
|
188
|
+
- Multi-`mediafinder` fields (`images` for `Gallery` / `ImageStrip`) are arrays of path strings — but must be empty `[]` per the rule above.
|
|
189
|
+
- `entries` links are integers: `form_entry` (block level for `Form`), `content.menu_entry` (for `MenuBlock`), `content.prefill_entry` (for `Prefill`).
|
|
190
|
+
- `Slider` accepts an optional `content.breakpoints = { tablet: number, mobile: number }`.
|
|
191
|
+
- `Columns` blocks have `content.columns = [{ blockId, builder: [...blocks] }]` — recursive: each column's `builder` follows the exact same shape as the top-level `builder`.
|
|
192
|
+
|
|
193
|
+
## Catalogue drift
|
|
194
|
+
|
|
195
|
+
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.
|
|
196
|
+
|
|
197
|
+
## Publishing
|
|
198
|
+
|
|
199
|
+
To publish to your GitLab group's npm registry:
|
|
200
|
+
|
|
201
|
+
1. Edit `package.json`:
|
|
202
|
+
- Replace `@yourorg` in `name` and `publishConfig["@yourorg:registry"]` with your actual GitLab scope/group.
|
|
203
|
+
- Replace `<YOUR-PROJECT-ID>` in `publishConfig["@yourorg:registry"]` with the numeric project id of the `frameworc-mcp` GitLab repo.
|
|
204
|
+
2. Create a GitLab deploy token / project access token with `api` + `write_registry` scope. Add a `.npmrc` at the registry host:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
@yourorg:registry=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/
|
|
208
|
+
//gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/:_authToken=<TOKEN>
|
|
209
|
+
```
|
|
210
|
+
3. `npm publish`.
|
|
211
|
+
|
|
212
|
+
Colleagues then use `npx -y @yourorg/frameworc-mcp` in their `claude_desktop_config.json` (no clone needed).
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
216
|
MIT.
|
package/dist/api-client.js
CHANGED
|
@@ -28,56 +28,103 @@ class ApiClient {
|
|
|
28
28
|
}
|
|
29
29
|
if (!res.ok) {
|
|
30
30
|
const msg = json && typeof json === "object" && json.errors
|
|
31
|
-
? `HTTP ${res.status}: ${JSON.stringify(json.errors)}`
|
|
31
|
+
? `HTTP ${res.status}: ${json.message ?? ""} ${JSON.stringify(json.errors)}`
|
|
32
32
|
: `HTTP ${res.status}: ${typeof json === "string" ? json : JSON.stringify(json)}`;
|
|
33
|
-
throw new Error(msg);
|
|
33
|
+
throw new Error(msg.trim());
|
|
34
34
|
}
|
|
35
35
|
return json;
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/** Appends the effective CMS site id to a query string. */
|
|
38
|
+
q(path, siteId) {
|
|
39
|
+
const id = siteId ?? this.session.getSiteId();
|
|
40
|
+
if (id === null || id === undefined)
|
|
41
|
+
return path;
|
|
42
|
+
return path + (path.includes("?") ? "&" : "?") + `site_id=${id}`;
|
|
39
43
|
}
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
/** Merges the effective CMS site id into a JSON body. */
|
|
45
|
+
b(body, siteId) {
|
|
46
|
+
const id = siteId ?? this.session.getSiteId();
|
|
47
|
+
const merged = { ...(body ?? {}) };
|
|
48
|
+
if (id !== null && id !== undefined && merged.site_id === undefined) {
|
|
49
|
+
merged.site_id = id;
|
|
50
|
+
}
|
|
51
|
+
return JSON.stringify(merged);
|
|
52
|
+
}
|
|
53
|
+
// --- sites & schema ---------------------------------------------------
|
|
54
|
+
listCmsSites() {
|
|
55
|
+
return this.request("/sites");
|
|
56
|
+
}
|
|
57
|
+
listBlocks() {
|
|
58
|
+
return this.request("/blocks");
|
|
59
|
+
}
|
|
60
|
+
getBlockSchema(name) {
|
|
61
|
+
return this.request(`/blocks/${encodeURIComponent(name)}`);
|
|
62
|
+
}
|
|
63
|
+
// --- pages ------------------------------------------------------------
|
|
64
|
+
listPages(siteId) {
|
|
65
|
+
return this.request(this.q("/pages", siteId));
|
|
66
|
+
}
|
|
67
|
+
getPage(id, siteId) {
|
|
68
|
+
return this.request(this.q(`/pages/${id}`, siteId));
|
|
42
69
|
}
|
|
43
|
-
createPage(payload) {
|
|
44
|
-
return this.request("/pages", { method: "POST", body:
|
|
70
|
+
createPage(payload, siteId) {
|
|
71
|
+
return this.request("/pages", { method: "POST", body: this.b(payload, siteId) });
|
|
45
72
|
}
|
|
46
|
-
updatePage(id, payload) {
|
|
47
|
-
return this.request(`/pages/${id}`, { method: "PATCH", body:
|
|
73
|
+
updatePage(id, payload, siteId) {
|
|
74
|
+
return this.request(`/pages/${id}`, { method: "PATCH", body: this.b(payload, siteId) });
|
|
48
75
|
}
|
|
49
|
-
deletePage(id) {
|
|
50
|
-
return this.request(`/pages/${id}`, { method: "DELETE" });
|
|
76
|
+
deletePage(id, siteId) {
|
|
77
|
+
return this.request(this.q(`/pages/${id}`, siteId), { method: "DELETE" });
|
|
51
78
|
}
|
|
52
|
-
|
|
79
|
+
createTranslation(id, targetSiteId, payload, sourceSiteId) {
|
|
80
|
+
const body = { ...(payload ?? {}), site_id: targetSiteId };
|
|
81
|
+
const source = sourceSiteId ?? this.session.getSiteId();
|
|
82
|
+
if (source !== null && source !== undefined) {
|
|
83
|
+
body.source_site_id = source;
|
|
84
|
+
}
|
|
85
|
+
return this.request(`/pages/${id}/translations`, { method: "POST", body: JSON.stringify(body) });
|
|
86
|
+
}
|
|
87
|
+
// --- blocks (per-page) ------------------------------------------------
|
|
88
|
+
addBlock(pageId, block, position, siteId) {
|
|
53
89
|
return this.request(`/pages/${pageId}/blocks`, {
|
|
54
90
|
method: "POST",
|
|
55
|
-
body:
|
|
91
|
+
body: this.b({ block, position }, siteId),
|
|
56
92
|
});
|
|
57
93
|
}
|
|
58
|
-
updateBlock(pageId, blockId, block) {
|
|
94
|
+
updateBlock(pageId, blockId, block, siteId) {
|
|
59
95
|
return this.request(`/pages/${pageId}/blocks/${blockId}`, {
|
|
60
96
|
method: "PATCH",
|
|
61
|
-
body:
|
|
97
|
+
body: this.b(block, siteId),
|
|
62
98
|
});
|
|
63
99
|
}
|
|
64
|
-
removeBlock(pageId, blockId) {
|
|
65
|
-
return this.request(`/pages/${pageId}/blocks/${blockId}`, { method: "DELETE" });
|
|
100
|
+
removeBlock(pageId, blockId, siteId) {
|
|
101
|
+
return this.request(this.q(`/pages/${pageId}/blocks/${blockId}`, siteId), { method: "DELETE" });
|
|
66
102
|
}
|
|
67
|
-
reorderBlocks(pageId, order) {
|
|
103
|
+
reorderBlocks(pageId, order, siteId) {
|
|
68
104
|
return this.request(`/pages/${pageId}/blocks/order`, {
|
|
69
105
|
method: "POST",
|
|
70
|
-
body:
|
|
106
|
+
body: this.b({ order }, siteId),
|
|
71
107
|
});
|
|
72
108
|
}
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
// --- referenced entries ----------------------------------------------
|
|
110
|
+
listForms(siteId) {
|
|
111
|
+
return this.request(this.q("/forms", siteId));
|
|
112
|
+
}
|
|
113
|
+
listMenus(siteId) {
|
|
114
|
+
return this.request(this.q("/menus", siteId));
|
|
75
115
|
}
|
|
76
|
-
|
|
77
|
-
return this.request("/
|
|
116
|
+
listPrefills(siteId) {
|
|
117
|
+
return this.request(this.q("/prefills", siteId));
|
|
78
118
|
}
|
|
79
|
-
|
|
80
|
-
|
|
119
|
+
// --- singles ----------------------------------------------------------
|
|
120
|
+
getSingle(handle, siteId) {
|
|
121
|
+
return this.request(this.q(`/singles/${encodeURIComponent(handle)}`, siteId));
|
|
122
|
+
}
|
|
123
|
+
updateSingle(handle, fields, siteId) {
|
|
124
|
+
return this.request(`/singles/${encodeURIComponent(handle)}`, {
|
|
125
|
+
method: "PATCH",
|
|
126
|
+
body: this.b(fields, siteId),
|
|
127
|
+
});
|
|
81
128
|
}
|
|
82
129
|
}
|
|
83
130
|
exports.ApiClient = ApiClient;
|
package/dist/catalogue.js
CHANGED
|
@@ -194,12 +194,12 @@ exports.BLOCKS = [
|
|
|
194
194
|
content_group: "Form",
|
|
195
195
|
name: "Form",
|
|
196
196
|
description: "Contact / inquiry form wired to a separately-defined Form Tailor entry.",
|
|
197
|
-
whenToUse: "Only when a contact form is needed AND the referenced Form entry already exists.
|
|
197
|
+
whenToUse: "Only when a contact form is needed AND the referenced Form entry already exists. Set the block-level `form` key to the entry id from the `list_forms` tool. The actual form fields live on the linked Form entry, not on this block.",
|
|
198
198
|
base: baseBlock,
|
|
199
199
|
content: [
|
|
200
200
|
{ name: "variant", type: "dropdown", enum: ["default", "outline", "card"], default: "default" },
|
|
201
201
|
],
|
|
202
|
-
referencing: { kind: "form", location: "block", jsonKey: "
|
|
202
|
+
referencing: { kind: "form", location: "block", jsonKey: "form", column: "form_id" },
|
|
203
203
|
},
|
|
204
204
|
{
|
|
205
205
|
content_group: "Gallery",
|
|
@@ -257,7 +257,7 @@ exports.BLOCKS = [
|
|
|
257
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.",
|
|
258
258
|
base: baseBlock,
|
|
259
259
|
content: [],
|
|
260
|
-
referencing: { kind: "prefill", location: "content", jsonKey: "
|
|
260
|
+
referencing: { kind: "prefill", location: "content", jsonKey: "block", column: "block_id" },
|
|
261
261
|
},
|
|
262
262
|
{
|
|
263
263
|
content_group: "BlogList",
|
|
@@ -276,10 +276,10 @@ exports.BLOCKS = [
|
|
|
276
276
|
content_group: "MenuBlock",
|
|
277
277
|
name: "Menu block",
|
|
278
278
|
description: "Renders an existing Menu Tailor entry inline in the page body (flattened to 2 levels).",
|
|
279
|
-
whenToUse: "When you need to embed navigation as content (e.g. an on-page section that lists links).
|
|
279
|
+
whenToUse: "When you need to embed navigation as content (e.g. an on-page section that lists links). Set `content.menu` to the entry id from `list_menus`.",
|
|
280
280
|
base: baseBlock,
|
|
281
281
|
content: [],
|
|
282
|
-
referencing: { kind: "menu", location: "content", jsonKey: "
|
|
282
|
+
referencing: { kind: "menu", location: "content", jsonKey: "menu", column: "menu_id" },
|
|
283
283
|
},
|
|
284
284
|
{
|
|
285
285
|
content_group: "ImageStrip",
|
package/dist/index.js
CHANGED
|
@@ -37,12 +37,34 @@ const tools = [
|
|
|
37
37
|
properties: {},
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
|
+
{
|
|
41
|
+
name: "list_cms_sites",
|
|
42
|
+
description: "List the multisite sites configured INSIDE the pinned OctoberCMS install (languages, regional variants). Returns [{id, name, code, locale, is_enabled, is_primary}]. Call this before creating content on a multi-language project so you know which site_id to target. Not to be confused with list_sites, which lists separate CMS installs from your local config file.",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: { site_url: { type: "string" } },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "use_cms_site",
|
|
50
|
+
description: "Pin which multisite site (language) subsequent calls read and write. Pass the id from list_cms_sites, or null to go back to the primary site. Every page/single tool also accepts a one-off site_id argument that overrides this.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
site_id: { type: ["integer", "null"], description: "Site id from list_cms_sites, or null for the primary site." },
|
|
55
|
+
},
|
|
56
|
+
required: ["site_id"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
40
59
|
{
|
|
41
60
|
name: "list_pages",
|
|
42
|
-
description: "List all FrameworC pages on the pinned site.",
|
|
61
|
+
description: "List all FrameworC pages on the pinned site (and pinned CMS site, if set).",
|
|
43
62
|
inputSchema: {
|
|
44
63
|
type: "object",
|
|
45
|
-
properties: {
|
|
64
|
+
properties: {
|
|
65
|
+
site_url: { type: "string", description: "Override the pinned site URL for this call." },
|
|
66
|
+
site_id: { type: "integer", description: "Override the pinned multisite site for this call." },
|
|
67
|
+
},
|
|
46
68
|
},
|
|
47
69
|
},
|
|
48
70
|
{
|
|
@@ -53,6 +75,7 @@ const tools = [
|
|
|
53
75
|
properties: {
|
|
54
76
|
id: { type: "integer", description: "Page id." },
|
|
55
77
|
site_url: { type: "string" },
|
|
78
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
56
79
|
},
|
|
57
80
|
required: ["id"],
|
|
58
81
|
},
|
|
@@ -73,6 +96,7 @@ const tools = [
|
|
|
73
96
|
required: ["page"],
|
|
74
97
|
},
|
|
75
98
|
site_url: { type: "string" },
|
|
99
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
76
100
|
},
|
|
77
101
|
required: ["payload"],
|
|
78
102
|
},
|
|
@@ -86,6 +110,7 @@ const tools = [
|
|
|
86
110
|
id: { type: "integer" },
|
|
87
111
|
payload: { type: "object" },
|
|
88
112
|
site_url: { type: "string" },
|
|
113
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
89
114
|
},
|
|
90
115
|
required: ["id", "payload"],
|
|
91
116
|
},
|
|
@@ -98,13 +123,14 @@ const tools = [
|
|
|
98
123
|
properties: {
|
|
99
124
|
id: { type: "integer" },
|
|
100
125
|
site_url: { type: "string" },
|
|
126
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
101
127
|
},
|
|
102
128
|
required: ["id"],
|
|
103
129
|
},
|
|
104
130
|
},
|
|
105
131
|
{
|
|
106
132
|
name: "add_block",
|
|
107
|
-
description: "Append a block to a page's builder. Optional `position` (1-indexed) inserts at that position; otherwise appends at the end. The `block` arg has the shape { content_group, base, content,
|
|
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`.",
|
|
108
134
|
inputSchema: {
|
|
109
135
|
type: "object",
|
|
110
136
|
properties: {
|
|
@@ -112,6 +138,7 @@ const tools = [
|
|
|
112
138
|
block: { type: "object" },
|
|
113
139
|
position: { type: "integer" },
|
|
114
140
|
site_url: { type: "string" },
|
|
141
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
115
142
|
},
|
|
116
143
|
required: ["page_id", "block"],
|
|
117
144
|
},
|
|
@@ -126,6 +153,7 @@ const tools = [
|
|
|
126
153
|
block_id: { type: "integer" },
|
|
127
154
|
block: { type: "object" },
|
|
128
155
|
site_url: { type: "string" },
|
|
156
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
129
157
|
},
|
|
130
158
|
required: ["page_id", "block_id", "block"],
|
|
131
159
|
},
|
|
@@ -139,6 +167,7 @@ const tools = [
|
|
|
139
167
|
page_id: { type: "integer" },
|
|
140
168
|
block_id: { type: "integer" },
|
|
141
169
|
site_url: { type: "string" },
|
|
170
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
142
171
|
},
|
|
143
172
|
required: ["page_id", "block_id"],
|
|
144
173
|
},
|
|
@@ -152,13 +181,14 @@ const tools = [
|
|
|
152
181
|
page_id: { type: "integer" },
|
|
153
182
|
order: { type: "array", items: { type: "integer" } },
|
|
154
183
|
site_url: { type: "string" },
|
|
184
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
155
185
|
},
|
|
156
186
|
required: ["page_id", "order"],
|
|
157
187
|
},
|
|
158
188
|
},
|
|
159
189
|
{
|
|
160
190
|
name: "list_forms",
|
|
161
|
-
description: "List Form Tailor entries available for the `Form` block's `
|
|
191
|
+
description: "List Form Tailor entries available for the `Form` block's block-level `form` key.",
|
|
162
192
|
inputSchema: {
|
|
163
193
|
type: "object",
|
|
164
194
|
properties: { site_url: { type: "string" } },
|
|
@@ -166,7 +196,7 @@ const tools = [
|
|
|
166
196
|
},
|
|
167
197
|
{
|
|
168
198
|
name: "list_menus",
|
|
169
|
-
description: "List Menu Tailor entries available for the `MenuBlock`'s `content.
|
|
199
|
+
description: "List Menu Tailor entries available for the `MenuBlock`'s `content.menu` key.",
|
|
170
200
|
inputSchema: {
|
|
171
201
|
type: "object",
|
|
172
202
|
properties: { site_url: { type: "string" } },
|
|
@@ -174,7 +204,7 @@ const tools = [
|
|
|
174
204
|
},
|
|
175
205
|
{
|
|
176
206
|
name: "list_prefills",
|
|
177
|
-
description: "List Prefill Tailor entries available for the `Prefill` block's `content.
|
|
207
|
+
description: "List Prefill Tailor entries available for the `Prefill` block's `content.block` key.",
|
|
178
208
|
inputSchema: {
|
|
179
209
|
type: "object",
|
|
180
210
|
properties: { site_url: { type: "string" } },
|
|
@@ -182,17 +212,63 @@ const tools = [
|
|
|
182
212
|
},
|
|
183
213
|
{
|
|
184
214
|
name: "get_block_schema",
|
|
185
|
-
description: "Get the JSON schema + usage notes for
|
|
215
|
+
description: "Get the JSON schema + usage notes for one block by content_group name. Fetched live from the CMS, which derives it from the actual Tailor blueprints, so it is always current. Use this to recall field enums, defaults and conditional visibility.",
|
|
186
216
|
inputSchema: {
|
|
187
217
|
type: "object",
|
|
188
218
|
properties: {
|
|
189
219
|
name: { type: "string", enum: catalogue_js_1.BLOCKS.map((b) => b.content_group) },
|
|
220
|
+
site_url: { type: "string" },
|
|
221
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
190
222
|
},
|
|
191
223
|
required: ["name"],
|
|
192
224
|
},
|
|
193
225
|
},
|
|
226
|
+
{
|
|
227
|
+
name: "get_page_meta",
|
|
228
|
+
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.",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: {
|
|
232
|
+
handle: { type: "string", enum: ["Meta", "Navigation", "Footer"] },
|
|
233
|
+
site_url: { type: "string" },
|
|
234
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
235
|
+
},
|
|
236
|
+
required: ["handle"],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
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 must be left empty. Affects every page on that site, so confirm with the user first.",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {
|
|
245
|
+
handle: { type: "string", enum: ["Meta", "Navigation", "Footer"] },
|
|
246
|
+
fields: { type: "object", description: "Field name => value map." },
|
|
247
|
+
site_url: { type: "string" },
|
|
248
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
249
|
+
},
|
|
250
|
+
required: ["handle", "fields"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
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.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
id: { type: "integer", description: "Source page id." },
|
|
260
|
+
target_site_id: { type: "integer", description: "Site to create the translation on." },
|
|
261
|
+
source_site_id: { type: "integer", description: "Site the source page lives on. Defaults to the pinned CMS site." },
|
|
262
|
+
page: { type: "object" },
|
|
263
|
+
builder: { type: "array" },
|
|
264
|
+
site_url: { type: "string" },
|
|
265
|
+
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
266
|
+
},
|
|
267
|
+
required: ["id", "target_site_id"],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
194
270
|
];
|
|
195
|
-
const server = new index_js_1.Server({ name: "frameworc-mcp", version: "0.
|
|
271
|
+
const server = new index_js_1.Server({ name: "frameworc-mcp", version: "0.3.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
196
272
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
197
273
|
tools,
|
|
198
274
|
}));
|
|
@@ -200,6 +276,10 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
|
200
276
|
const name = req.params.name;
|
|
201
277
|
const args = (req.params.arguments ?? {});
|
|
202
278
|
(0, sites_js_1.refreshIfChanged)();
|
|
279
|
+
// A one-off site_id argument overrides the pinned CMS site for this call.
|
|
280
|
+
const site = args.site_id !== undefined && args.site_id !== null
|
|
281
|
+
? Number(args.site_id)
|
|
282
|
+
: undefined;
|
|
203
283
|
if (name !== "use_site" && name !== "list_sites" && !args.site_url && !session.getUrl()) {
|
|
204
284
|
return {
|
|
205
285
|
content: [
|
|
@@ -254,68 +334,108 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
|
254
334
|
content: [{ type: "text", text: JSON.stringify({ sites }, null, 2) }],
|
|
255
335
|
};
|
|
256
336
|
}
|
|
337
|
+
case "list_cms_sites": {
|
|
338
|
+
const r = await client.listCmsSites();
|
|
339
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
340
|
+
}
|
|
341
|
+
case "use_cms_site": {
|
|
342
|
+
const id = args.site_id === null || args.site_id === undefined ? null : Number(args.site_id);
|
|
343
|
+
session.setSiteId(id);
|
|
344
|
+
return {
|
|
345
|
+
content: [{
|
|
346
|
+
type: "text",
|
|
347
|
+
text: id === null
|
|
348
|
+
? "Using the primary CMS site."
|
|
349
|
+
: `Using CMS site id ${id} for subsequent calls.`,
|
|
350
|
+
}],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
257
353
|
case "list_pages": {
|
|
258
|
-
const r = await client.listPages();
|
|
354
|
+
const r = await client.listPages(site);
|
|
259
355
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
260
356
|
}
|
|
261
357
|
case "get_page": {
|
|
262
|
-
const r = await client.getPage(Number(args.id));
|
|
358
|
+
const r = await client.getPage(Number(args.id), site);
|
|
263
359
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
264
360
|
}
|
|
265
361
|
case "create_page": {
|
|
266
|
-
const r = await client.createPage(args.payload);
|
|
362
|
+
const r = await client.createPage(args.payload, site);
|
|
267
363
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
268
364
|
}
|
|
269
365
|
case "update_page": {
|
|
270
|
-
const r = await client.updatePage(Number(args.id), args.payload);
|
|
366
|
+
const r = await client.updatePage(Number(args.id), args.payload, site);
|
|
271
367
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
272
368
|
}
|
|
273
369
|
case "delete_page": {
|
|
274
|
-
const r = await client.deletePage(Number(args.id));
|
|
370
|
+
const r = await client.deletePage(Number(args.id), site);
|
|
371
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
372
|
+
}
|
|
373
|
+
case "create_translation": {
|
|
374
|
+
const r = await client.createTranslation(Number(args.id), Number(args.target_site_id), { page: args.page ?? {}, builder: args.builder }, args.source_site_id !== undefined ? Number(args.source_site_id) : undefined);
|
|
275
375
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
276
376
|
}
|
|
277
377
|
case "add_block": {
|
|
278
|
-
const r = await client.addBlock(Number(args.page_id), args.block, args.position !== undefined ? Number(args.position) : undefined);
|
|
378
|
+
const r = await client.addBlock(Number(args.page_id), args.block, args.position !== undefined ? Number(args.position) : undefined, site);
|
|
279
379
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
280
380
|
}
|
|
281
381
|
case "update_block": {
|
|
282
|
-
const r = await client.updateBlock(Number(args.page_id), Number(args.block_id), args.block);
|
|
382
|
+
const r = await client.updateBlock(Number(args.page_id), Number(args.block_id), args.block, site);
|
|
283
383
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
284
384
|
}
|
|
285
385
|
case "remove_block": {
|
|
286
|
-
const r = await client.removeBlock(Number(args.page_id), Number(args.block_id));
|
|
386
|
+
const r = await client.removeBlock(Number(args.page_id), Number(args.block_id), site);
|
|
287
387
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
288
388
|
}
|
|
289
389
|
case "reorder_blocks": {
|
|
290
|
-
const r = await client.reorderBlocks(Number(args.page_id), args.order);
|
|
390
|
+
const r = await client.reorderBlocks(Number(args.page_id), args.order, site);
|
|
291
391
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
292
392
|
}
|
|
293
393
|
case "list_forms": {
|
|
294
|
-
const r = await client.listForms();
|
|
394
|
+
const r = await client.listForms(site);
|
|
295
395
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
296
396
|
}
|
|
297
397
|
case "list_menus": {
|
|
298
|
-
const r = await client.listMenus();
|
|
398
|
+
const r = await client.listMenus(site);
|
|
299
399
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
300
400
|
}
|
|
301
401
|
case "list_prefills": {
|
|
302
|
-
const r = await client.listPrefills();
|
|
402
|
+
const r = await client.listPrefills(site);
|
|
403
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
404
|
+
}
|
|
405
|
+
case "get_page_meta": {
|
|
406
|
+
const r = await client.getSingle(String(args.handle), site);
|
|
407
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
408
|
+
}
|
|
409
|
+
case "update_page_meta": {
|
|
410
|
+
const r = await client.updateSingle(String(args.handle), args.fields ?? {}, site);
|
|
303
411
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
304
412
|
}
|
|
305
413
|
case "get_block_schema": {
|
|
306
|
-
|
|
307
|
-
|
|
414
|
+
// Served by the CMS so it always matches the live blueprints; the
|
|
415
|
+
// bundled catalogue is only a fallback for an older plugin version.
|
|
416
|
+
try {
|
|
417
|
+
const r = await client.getBlockSchema(String(args.name));
|
|
418
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
const block = catalogue_js_1.BLOCKS.find((b) => b.content_group === args.name);
|
|
422
|
+
if (!block) {
|
|
423
|
+
return {
|
|
424
|
+
content: [{
|
|
425
|
+
type: "text",
|
|
426
|
+
text: `Unknown block "${args.name}". Available: ${catalogue_js_1.BLOCKS.map((b) => b.content_group).join(", ")}`,
|
|
427
|
+
}],
|
|
428
|
+
isError: true,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
308
431
|
return {
|
|
309
|
-
content: [
|
|
310
|
-
{
|
|
432
|
+
content: [{
|
|
311
433
|
type: "text",
|
|
312
|
-
text: `
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
isError: true,
|
|
434
|
+
text: `(served from the bundled catalogue; live schema unavailable: ${err?.message ?? err})\n`
|
|
435
|
+
+ JSON.stringify(block, null, 2),
|
|
436
|
+
}],
|
|
316
437
|
};
|
|
317
438
|
}
|
|
318
|
-
return { content: [{ type: "text", text: JSON.stringify(block, null, 2) }] };
|
|
319
439
|
}
|
|
320
440
|
default: {
|
|
321
441
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
package/dist/session.js
CHANGED
|
@@ -6,6 +6,8 @@ const sites_js_1 = require("./sites.js");
|
|
|
6
6
|
class Session {
|
|
7
7
|
url = null;
|
|
8
8
|
tokenOverride = null;
|
|
9
|
+
/** The CMS multisite site to read and write. Null means the primary site. */
|
|
10
|
+
siteId = null;
|
|
9
11
|
id;
|
|
10
12
|
constructor() {
|
|
11
13
|
this.id = (0, node_crypto_1.randomUUID)();
|
|
@@ -18,6 +20,14 @@ class Session {
|
|
|
18
20
|
else {
|
|
19
21
|
this.tokenOverride = null;
|
|
20
22
|
}
|
|
23
|
+
// A different install has its own site ids.
|
|
24
|
+
this.siteId = null;
|
|
25
|
+
}
|
|
26
|
+
setSiteId(siteId) {
|
|
27
|
+
this.siteId = siteId;
|
|
28
|
+
}
|
|
29
|
+
getSiteId() {
|
|
30
|
+
return this.siteId;
|
|
21
31
|
}
|
|
22
32
|
getToken() {
|
|
23
33
|
if (!this.url) {
|
|
@@ -41,6 +51,7 @@ class Session {
|
|
|
41
51
|
clear() {
|
|
42
52
|
this.url = null;
|
|
43
53
|
this.tokenOverride = null;
|
|
54
|
+
this.siteId = null;
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
exports.Session = Session;
|
package/package.json
CHANGED