frameworc-mcp
v0.4.0
Published
MCP server for the FrameworC OctoberCMS plugin — programmatically create/edit FrameworC pages and blocks.
Readme
@yourorg/frameworc-mcp
A Model Context Protocol 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.
What it does
- Composes 15 prebuilt FrameworC blocks (
Header,Section,Tiles,Slider,Tabs,Accordion,Form,Gallery,Downloads,Columns,Prefill,BlogList,MenuBlock,ImageStrip,InstaFeed) into pages - Lists existing pages and their full builder JSON
- Creates pages, adds / updates / removes / reorders individual blocks — on pages and on Prefill entries
- Full CRUD for
Formentries (incl. their field rows),Menuentries (incl. the navigation tree) andPrefillentries (incl. their builder blocks) - 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 - 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)
- Multisite-aware: every content tool takes/pins a
site_id; page and prefill translations are linked for the language switcher - 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 - Draft by default — created pages have
is_enabled = false; the human flips the switch in the OctCMS backend after assigning images
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.
Architecture
chat client (Claude Desktop / opencode / Cursor / Cline / Continue / ...)
│ stdio JSON-RPC
▼
node dist/index.js (local, spawned by the chat client)
reads ~/.config/frameworc/sites.json (auto-created on first run; hot-reloaded)
│ HTTPS + Authorization: Bearer <token resolved by URL>
▼
any OctCMS install /api/mcp/v1/* (provided by the crscompany/frameworcmcp plugin v1.1.0+)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.
Install
git clone <your-gitlab-url>/frameworc-mcp.git
cd frameworc-mcp
npm install
npm run build(Optional) publish to your GitLab group's npm registry so colleagues can npx it without a clone — see Publishing below.
Claude Desktop config
Edit claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/; Linux: ~/.config/Claude/):
{
"mcpServers": {
"frameworc": {
"command": "npx",
"args": ["-y", "@yourorg/frameworc-mcp"]
}
}
}If you cloned instead of npx-installed, point args to your local dist/index.js:
{
"mcpServers": {
"frameworc": {
"command": "node",
"args": ["/absolute/path/to/frameworc-mcp/dist/index.js"]
}
}
}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).
Per-site token file — ~/.config/frameworc/sites.json
The MCP stores one bearer token per OctCMS install in this file on your laptop. It's the only credential store. Format:
{
"sites": [
{ "label": "Client A prod", "url": "https://clienta.test", "token": "AAA..." },
{ "label": "Client A staging", "url": "https://staging.clienta.test", "token": "BBB..." },
{ "label": "Myco", "url": "https://myco.example", "token": "CCC..." }
]
}labelis optional, shown by thelist_sitestool so the chat can pick a site by name when you say "use the myco site".urlis matched after normalising trailing slashes —https://x.test/andhttps://x.testare the same.tokenis sent asAuthorization: Bearer <token>on every HTTP call to that site. Never committed to git.
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.
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.
Adding a new OctCMS site
- Spin up the new OctCMS install with the FrameworC suite incl. the
crscompany/frameworcmcpplugin (v1.1.0 or newer). - Backend → Settings → FrameworC → MCP API → paste a freshly generated random string (e.g.
openssl rand -hex 32) → Save. - Edit
~/.config/frameworc/sites.jsonon your laptop, add one entry:{ "label": "New Client", "url": "https://newsite.test", "token": "<that string>" }. - In your next chat (no Claude Desktop restart needed): "Use
https://newsite.test." → MCP hot-reloads the file, finds the token, sends it. Done.
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.
Security
- The file should be
0600(auto-set on first creation). If youchmodit looser, the MCP prints a stderr warning on startup. - 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.
- 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.
Tools (0.4.0)
| Tool | Description |
|---|---|
| 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. |
| list_sites | List configured sites (label + URL only, no tokens) from sites.json. |
| list_cms_sites | List the multisite sites (languages) inside the pinned install. |
| use_cms_site(site_id) | Pin the multisite site for subsequent calls (null = primary). |
| list_pages | List all pages on the pinned site. |
| get_page(id) | Full page JSON (page meta + builder blocks + nested repeaters). |
| create_page(payload) | Create a draft page. payload = { page: {...}, builder: [...] }. |
| update_page(id, payload) | Edit page meta, or full-rebuild the builder array (prefer the per-block tools). |
| delete_page(id) | Soft-delete a page. |
| create_translation(id, target_site_id, prefill?) | Linked sibling of a page (or Prefill with prefill:true) on another multisite site. |
| add_block(page_id \| prefill_id, block, position?) | Append (or insert at position) a block. |
| update_block(page_id \| prefill_id, block_id, block) | Replace one block by row id (new id returned; human-assigned media carried over unless the type changes). |
| remove_block(page_id \| prefill_id, block_id) | Remove one block by row id. |
| reorder_blocks(page_id \| prefill_id, order) | Reorder blocks (order = array of all row ids in new order). |
| extract_block_to_prefill(page_id, block_id, title) | Move a page block into a new Prefill entry (lossless, media survives) and reference it in place. |
| list_forms / get_form(id) | Form entries; get_form includes the fwcFields rows. |
| get_form_schema | Live field-group catalogue for authoring forms. |
| create_form / update_form / delete_form | Form CRUD. fwcFields rows: {group, label, name, required, width, ...}; delete guarded unless force:true. |
| list_menus / get_menu(id) | Menu entries; get_menu includes the navigation tree. |
| create_menu / update_menu / delete_menu | Menu CRUD. Tree items {title, url \| {page_id}, anchor, blank, children}, max 2 levels; delete guarded. |
| list_prefills / get_prefill(id) | Prefill entries; get_prefill includes the builder blocks. |
| create_prefill / update_prefill / delete_prefill | Prefill CRUD — same block shape as pages; delete guarded. |
| 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). |
| get_settings / update_settings(fields) | Global FrameworC settings: navbar options + custom SCSS variables. Integration secrets are not exposed. |
| get_block_schema(name) | Field schema + usage notes for one block (live from the CMS). |
Form and Menu entries have no translation linking — create them per site by passing site_id.
Resource
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.
Page-content JSON shape
create_page / update_page payload:
{
"page": {
"title": "Contact",
"slug": "contact",
"fullslug": "contact",
"is_enabled": false,
"metaTitle": "",
"metaDescription": "",
"menuStyle": "solid",
"menuHide": "no"
},
"builder": [
{
"content_group": "Header",
"base": {
"blockId": "kontakt",
"headline": "<h1>Contact us</h1>",
"elevated": false,
"containerWidth": "default",
"backgroundColor": "default",
"customCssClass": [],
"responsiveHide": []
},
"content": {
"image": "",
"imageMobile": "",
"isVideoBg": false,
"buttonLabel1": "",
"buttonLink1": "",
"buttonBlank1": false,
"fullHeight": true,
"contrast": false,
"overlay": false
}
},
{
"content_group": "Form",
"base": { "blockId": "formular", "headline": "<h2>Write us</h2>" },
"form": 7,
"content": { "variant": "default" }
}
]
}Storage encoding (what the MCP understands)
- 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. switchfields accept booleans; the API normalises to"1"/"0"for storage.customCssClass+responsiveHideare arrays of strings.- Multi-
mediafinderfields (imagesforGallery/ImageStrip) are arrays of path strings — but must be empty[]per the rule above. entrieslinks are integers (or{id: n}):form(block level forForm),content.menu(forMenuBlock),content.block(forPrefill). Reads return them as{id, title}.Slideraccepts an optionalcontent.breakpoints = { tablet: number, mobile: number }.Columnsblocks havecontent.columns = [{ blockId, builder: [...blocks] }]— recursive: each column'sbuilderfollows the exact same shape as the top-levelbuilder.
Catalogue drift
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.
Publishing
To publish to your GitLab group's npm registry:
Edit
package.json:- Replace
@yourorginnameandpublishConfig["@yourorg:registry"]with your actual GitLab scope/group. - Replace
<YOUR-PROJECT-ID>inpublishConfig["@yourorg:registry"]with the numeric project id of theframeworc-mcpGitLab repo.
- Replace
Create a GitLab deploy token / project access token with
api+write_registryscope. Add a.npmrcat the registry host:@yourorg:registry=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/ //gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/:_authToken=<TOKEN>npm publish.
Colleagues then use npx -y @yourorg/frameworc-mcp in their claude_desktop_config.json (no clone needed).
License
MIT.
