mcp-magento-cms 1.0.3 → 1.2.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,74 +1,73 @@
1
1
  # Magento CMS MCP (Cursor)
2
2
 
3
- Create/update Magento **CMS Pages / Blocks** with Page Builder **Row → Text** from Cursor chat.
3
+ Create/update Magento **CMS Pages / Blocks** with Page Builder from Cursor chat — including **validate/repair** for stage-safe HTML and **raw Page Builder HTML** saves for complex layouts.
4
4
 
5
- - **AI:** Cursor plans the content
6
- - **This server:** Magento REST via an Integration Access Token
7
- - **Install:** `npx` (no local clone, no `tools/` folder, no `.env` file required)
5
+ - **AI:** Cursor plans content and diagnostics
6
+ - **This server:** Magento REST via Integration Access Token
7
+ - **Local:** run from this repo path in `mcp.json` (point `MAGENTO_BASE_URL` at the target store)
8
+ - **Published:** `npx -y mcp-magento-cms` (npm)
8
9
 
9
10
  Requires **Node.js 18+** and [Cursor](https://cursor.com).
10
11
 
11
- Package: [npmjs.com/package/mcp-magento-cms](https://www.npmjs.com/package/mcp-magento-cms)
12
+ ## CMS workflow (mandatory for agents & teams)
12
13
 
13
- ## Tools
14
-
15
- | Tool | Purpose |
16
- |------|---------|
17
- | `magento_health` | Test base URL + token |
18
- | `get_cms_page` | Load by `page_id` |
19
- | `create_cms_page` | Create from title + content |
20
- | `update_cms_page` | Update by `page_id` |
21
- | `get_cms_block` / `create_cms_block` / `update_cms_block` | Blocks |
22
- | `list_section_types` | Supported layouts (MVP: `simple_text`) |
23
-
24
- ## Setup (any machine)
14
+ **Use this MCP as the only way to create or update Magento CMS pages/blocks.**
25
15
 
26
- You do **not** need to clone this repo or create a `.env` file. Cursor runs the package with `npx` and reads credentials from `mcp.json`.
16
+ | Do | Do not |
17
+ |----|--------|
18
+ | `pagebuilder_rules` → `compose_pagebuilder` / `create_cms_page` / `set_pagebuilder_html` | Project PHP `tools/compose-*.php` / `deploy-*.php` |
19
+ | `validate_pagebuilder` / `repair_pagebuilder` before save | Direct DB edits to `cms_page` / `cms_block` content |
20
+ | Improve **this package** when something is missing or wrong | Bypass MCP with one-off scripts |
27
21
 
28
- ### 1. Magento Integration Access Token
22
+ Preferred path: `sections[]` → `compose_pagebuilder` (or `create_cms_page` with `sections`) → validate → save. Use `set_pagebuilder_html` only for intentional raw master HTML. After save, verify on the storefront.
29
23
 
30
- No clone or `tools/` folder is needed. Create the token in Magento Admin:
24
+ Reload/restart the MCP server in Cursor after changing this package so teammates pick up new rules and tools.
31
25
 
32
- 1. **System → Extensions → Integrations → Add New Integration**
33
- 2. Name it (e.g. `MCP CMS`) and set your admin password when prompted
34
- 3. Open the **API** tab → set **Resource Access** to **Custom**
35
- 4. Under **Content → Elements**, select:
26
+ **REST field note (Magento 2.4.8):** `cmsPage` create/update use `active` (not `is_active`) and reject `store_id` / unknown fields. The client maps `is_active` → `active` so tool args stay familiar.
36
27
 
37
- | Select | Notes |
38
- |--------|--------|
39
- | **Pages** | Required (parent) |
40
- | → **Save Page** | Create / update pages |
41
- | → **Edit Page Design** | Page Builder / design fields |
42
- | → **Delete Page** | Optional (MCP does not delete today) |
43
- | **Blocks** | Required (create / update blocks) |
28
+ **Store views:** Magento REST cannot set `cms_page_store` / `cms_block_store`. On create, this MCP assigns **All Store Views** (`store_id = 0`) by default via Magento PHP (`scripts/assign-stores.php` + `ddev exec` when `.ddev` exists). Pass `store_ids: [1, 2]` (etc.) only when the user asks for specific store view(s). Requires `MAGENTO_ROOT` (or infer from `MAGENTO_CMS_PROJECT_RULES` → project root).
44
29
 
45
- Leave everything else unchecked (Staging, Widgets, Media Gallery, Amasty, Blog, Design Themes, etc.).
30
+ **Page layout:** On create, default `page_layout` is **`cms-full-width`** (admin label: Page — Full Width). Pass `page_layout` only when the user asks for a different layout (e.g. `1column`).
46
31
 
47
- 5. **Save → Activate** → copy the **Access Token** into `mcp.json` (step 3)
32
+ ## Shared rules (all projects — read first)
48
33
 
49
- Magento **2.4.4+** blocks integration tokens as Bearer by default. Enable once (either way):
34
+ Returned by `pagebuilder_rules` for **every** store. This package stays free of store-specific hex / class catalogs; those go in optional project rules.
50
35
 
51
- **Admin UI:** **Stores → Configuration → Services → OAuth → Consumer Settings** → set **Allow OAuth Access Tokens to be used as standalone Bearer tokens** to **Yes** → Save Config.
36
+ ### Page Builder (stage-safe)
52
37
 
53
- **CLI:**
38
+ 1. Rows: use **Full Width** or **Contained**. **Never Full Bleed** unless the user explicitly asks.
39
+ 2. When edge-to-edge hero is intentional: `allow_full_bleed=true` on validate/set/repair.
40
+ 3. `full-width` rows **MUST** include `.row-full-width-inner[data-element=inner]`.
41
+ 4. `contained` rows **MUST** include `[data-element=inner]`.
42
+ 5. Converting `full-bleed` → `full-width` without adding the inner wrapper breaks admin Page Builder.
43
+ 6. Every `[data-pb-style=ID]` in `<style>` must match an element — **orphans crash** the stage.
44
+ 7. Every column needs `width:%` in the style block.
45
+ 8. Avoid raw `<svg>` inside Text — use **Image** (or theme CSS) for icons.
46
+ 9. Hero CTAs: Banner **Show Button=Always** + Link URL.
47
+ 10. Links: prefer `{{store url=path}}` with an unquoted path (avoid `&quot;` in href).
48
+ 11. Prefer native content types; CMS body = page content only (layout owns chrome).
54
49
 
55
- ```bash
56
- bin/magento config:set oauth/consumer/enable_integration_as_bearer 1
57
- bin/magento cache:flush config
58
- ```
50
+ ### CMS media + theme conventions (all projects)
59
51
 
60
- (With ddev: prefix CLI with `ddev exec`.)
52
+ 12. **One flat media folder per CMS page** under `pub/media/wysiwyg/` — no `cards/`, `icons/`, `products/`, `related/` subfolders.
53
+ 13. **Folder name = CMS page URL key** (identifier), e.g. `wysiwyg/markets/market-industrial/hero.jpg`.
54
+ 14. After renaming media folders, update all `{{media url=…}}` in `cms_page` / `cms_block`, then flush cache.
55
+ 15. Sibling pages share **one** Magento_Cms LESS partial + shared classes — do not copy near-identical LESS into theme `_sources.less` per page.
56
+ 16. Page-only LESS → theme `Magento_Cms/web/css/source/` + `_module.less` import.
57
+ 17. Brand primary/secondary once in `_theme.less` (`@theme__color__primary` / `@theme__color__secondary`); use vars, not hex, in page LESS.
58
+ 18. **No module-level re-aliases** of theme vars; **no redundant `font-family`** when body already uses the theme base font.
59
+ 19. Scope page CSS with layout body class and/or content markers (`:has(.…)`).
61
60
 
62
- ### 2. Magento base URL
61
+ Use tools `pagebuilder_rules`, `validate_pagebuilder`, and `repair_pagebuilder`.
63
62
 
64
- Use your storefront base URL **without** a trailing slash, for example:
63
+ ## Per-store project rules (optional)
65
64
 
66
- - Production: `https://www.example.com`
67
- - Local ddev: `https://your-project.ddev.site` (`ddev describe` shows the URL)
65
+ Keep the published MCP **general**. Put store-specific values (brand hex, Vendor/theme, URL keys, class prefixes) in the **Magento project**, not in this package.
68
66
 
69
- ### 3. Add the MCP in Cursor
67
+ **Recommended (project-wise Cursor config):**
70
68
 
71
- **Cursor → Settings → MCP → Edit Config**, or create `.cursor/mcp.json` in your project:
69
+ 1. Commit `.cursor/magento-cms-project-rules.json` in the Magento repo.
70
+ 2. Add project MCP config at `.cursor/mcp.json` (Customize → MCPs → workspace) with `MAGENTO_ROOT` only — rules auto-load from `${MAGENTO_ROOT}/.cursor/magento-cms-project-rules.json`.
72
71
 
73
72
  ```json
74
73
  {
@@ -77,68 +76,160 @@ Use your storefront base URL **without** a trailing slash, for example:
77
76
  "command": "npx",
78
77
  "args": ["-y", "mcp-magento-cms"],
79
78
  "env": {
80
- "MAGENTO_BASE_URL": "https://www.example.com",
81
- "MAGENTO_ACCESS_TOKEN": "YOUR_INTEGRATION_ACCESS_TOKEN"
79
+ "MAGENTO_BASE_URL": "https://your-store.example/",
80
+ "MAGENTO_ACCESS_TOKEN": "${env:MAGENTO_ACCESS_TOKEN}",
81
+ "MAGENTO_ROOT": "${workspaceFolder}"
82
82
  }
83
83
  }
84
84
  }
85
85
  }
86
86
  ```
87
87
 
88
- **Local ddev / mkcert only** — Node may not trust the local TLS cert. Add:
88
+ Optional override: `MAGENTO_CMS_PROJECT_RULES=/path/to/file.json` if the rules file is not under `.cursor/`.
89
+
90
+ `pagebuilder_rules` appends project rules after shared rules. JSON shape:
89
91
 
90
92
  ```json
91
- "NODE_EXTRA_CA_CERTS": "/absolute/path/to/mkcert/rootCA.pem"
93
+ {
94
+ "rules": ["store-only convention"],
95
+ "detail": { "brand_tokens": "optional longer notes" }
96
+ }
92
97
  ```
93
98
 
94
- Typical Linux path: `$HOME/.local/share/mkcert/rootCA.pem`
99
+ Markdown/text files are also supported (one rule per non-empty, non-`#` line). Projects with no rules file still get all shared conventions.
100
+
95
101
 
96
- Optional env vars:
102
+ ## Compose with sections[]
97
103
 
98
- | Variable | Default | Purpose |
99
- |----------|---------|---------|
100
- | `MAGENTO_STORE_CODE` | (empty) | REST store code |
101
- | `MAGENTO_ADMIN_PATH` | `admin` | Admin path used in edit links |
104
+ Prefer structured trees over project-specific PHP. Theme CSS class names go in `css_classes` (project owns naming).
102
105
 
103
- ### 4. Restart MCP
106
+ ```js
107
+ // Tool: compose_pagebuilder OR create_cms_page({ title, sections })
108
+ {
109
+ "sections": [
110
+ {
111
+ "type": "row",
112
+ "appearance": "full-bleed",
113
+ "children": [{
114
+ "type": "banner",
115
+ "background_image": "{{media url=wysiwyg/hero.jpg}}",
116
+ "content_html": "<h1>Title</h1>",
117
+ "show_button": "always",
118
+ "button_text": "Contact",
119
+ "link_url": "#contact"
120
+ }]
121
+ },
122
+ {
123
+ "type": "row",
124
+ "appearance": "contained",
125
+ "children": [
126
+ { "type": "heading", "text": "Section", "heading_type": "h2" },
127
+ { "type": "text", "html": "<p>Body</p>" },
128
+ {
129
+ "type": "columns",
130
+ "columns": [
131
+ { "children": [
132
+ { "type": "image", "src": "{{media url=wysiwyg/a.jpg}}", "alt": "A" },
133
+ { "type": "text", "html": "<p>Card</p>" }
134
+ ]}
135
+ ]
136
+ },
137
+ { "type": "buttons", "items": [{ "text": "Shop", "url": "{{store url=shop}}" }] },
138
+ { "type": "divider" },
139
+ { "type": "products", "skus": ["SKU1", "SKU2"] },
140
+ { "type": "block", "identifier": "my-cms-block" }
141
+ ]
142
+ }
143
+ ]
144
+ }
145
+ ```
104
146
 
105
- In Cursor MCP settings, refresh/restart the `magento-cms` server until it shows connected.
147
+ Native types: `row`, `columns` / `column-group` / `column`, `text`, `heading`, `image`, `buttons`, `banner`, `html`, `divider`, `video`, `map`, `tabs`, `slider`, `products`, `block`.
106
148
 
107
- ### 5. Verify
149
+ Call `list_section_types` for the full parameter catalog. For intentional edge-to-edge heroes use `appearance: "full-bleed"` and `allow_full_bleed: true` on save.
108
150
 
109
- In Cursor chat:
151
+ ## Tools
110
152
 
111
- ```text
112
- Use magento_health to verify Magento.
113
- ```
153
+ | Tool | Purpose |
154
+ |------|---------|
155
+ | `magento_health` | Test base URL + token |
156
+ | `pagebuilder_rules` | Return Full Width / inner / orphan-style rules |
157
+ | `list_section_types` | Native element catalog + composition patterns |
158
+ | `compose_pagebuilder` | Build master-format HTML from `sections[]` (all PB types); optional save |
159
+ | `search_cms` | Find pages/blocks by identifier |
160
+ | `get_cms_page` / `get_cms_block` | Load entity (`include_content`, `validate`) |
161
+ | `validate_pagebuilder` | Stage-safety checks (html / page_id / block_id) |
162
+ | `repair_pagebuilder` | Fix orphans, full-bleed, inners, widths, SVG (`apply=true` to save) |
163
+ | `set_pagebuilder_html` | Save **raw** PB HTML without wiping to simple_text |
164
+ | `create_cms_page` / `update_cms_page` | Simple Row→Text composer |
165
+ | `create_cms_block` / `update_cms_block` | Simple Row→Text composer |
114
166
 
115
- Then try:
167
+ **Important:** `update_cms_page` / `update_cms_block` with `content` rebuild as **simple_text** only. For complex pages use `set_pagebuilder_html` or `repair_pagebuilder` with `apply=true`.
116
168
 
117
- ```text
118
- Create a CMS page titled "MCP Demo" with this content:
119
- First paragraph.
120
- Second paragraph.
121
- ```
169
+ ## Local Cursor config (this machine)
122
170
 
123
- ```text
124
- Update CMS page_id 8 with content: Updated body for page 8.
171
+ ```json
172
+ {
173
+ "mcpServers": {
174
+ "magento-cms": {
175
+ "command": "node",
176
+ "args": [
177
+ "/home/ajaykumar.prajapati@brainvire.com/htdocs/mcp-magento-cms/src/index.js"
178
+ ],
179
+ "env": {
180
+ "MAGENTO_BASE_URL": "https://your-store.ddev.site",
181
+ "MAGENTO_ACCESS_TOKEN": "YOUR_TOKEN",
182
+ "NODE_TLS_REJECT_UNAUTHORIZED": "0"
183
+ }
184
+ }
185
+ }
186
+ }
125
187
  ```
126
188
 
127
- ## Security
189
+ After changing this package, **reload MCP** in Cursor (Settings → MCP → refresh / restart).
190
+
191
+ ## Setup (published npx)
128
192
 
129
- - Put tokens only in `mcp.json` / Cursor env (do not commit them)
130
- - Give the Magento integration **CMS-only** permissions
193
+ ### 1. Magento Integration Access Token
131
194
 
132
- ## Requirements
195
+ 1. **System → Extensions → Integrations → Add New Integration**
196
+ 2. API → Custom → Content → Pages (Save/Edit Design) + Blocks
197
+ 3. Activate → copy Access Token
133
198
 
134
- - Node.js **18+** on the machine running Cursor
135
- - Magento 2 with REST and an Integration token
136
- - Network access from that machine to `MAGENTO_BASE_URL`
199
+ Enable Bearer tokens (2.4.4+):
137
200
 
138
- ## For package maintainers
201
+ ```bash
202
+ bin/magento config:set oauth/consumer/enable_integration_as_bearer 1
203
+ bin/magento cache:flush config
204
+ ```
139
205
 
140
- Clone this repo to develop or republish. Bump `version` in `package.json`, then:
206
+ ### 2. mcp.json with npx
207
+
208
+ ```json
209
+ {
210
+ "mcpServers": {
211
+ "magento-cms": {
212
+ "command": "npx",
213
+ "args": ["-y", "mcp-magento-cms"],
214
+ "env": {
215
+ "MAGENTO_BASE_URL": "https://www.example.com",
216
+ "MAGENTO_ACCESS_TOKEN": "YOUR_INTEGRATION_ACCESS_TOKEN"
217
+ }
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ Local ddev TLS: add `"NODE_TLS_REJECT_UNAUTHORIZED": "0"`.
224
+
225
+ ## Scripts
141
226
 
142
227
  ```bash
143
- npm publish --access public
228
+ npm run test:compose
229
+ npm run test:validate
230
+ npm run health
144
231
  ```
232
+
233
+ ## Version
234
+
235
+ `1.2.0` — compose all native Page Builder content types via `sections[]` + validate/repair/raw HTML.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-magento-cms",
3
- "version": "1.0.3",
4
- "description": "Cursor MCP: Magento CMS pages/blocks with Page Builder Row→Text (zero npm deps)",
3
+ "version": "1.2.0",
4
+ "description": "Cursor MCP: Magento CMS pages/blocks + Page Builder compose/validate/repair for all native content types (zero npm deps)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -14,7 +14,8 @@
14
14
  "scripts": {
15
15
  "start": "node src/index.js",
16
16
  "health": "node scripts/health-check.js",
17
- "test:compose": "node scripts/test-compose.js"
17
+ "test:compose": "node scripts/test-compose.js",
18
+ "test:validate": "node scripts/test-validate-repair.js"
18
19
  },
19
20
  "engines": {
20
21
  "node": ">=18"
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Assign CMS page/block to Magento store view(s).
3
+ *
4
+ * Magento 2.4.8 REST cmsPage/cmsBlock reject store_id on create/update, so we
5
+ * assign via Magento bootstrap PHP when MAGENTO_ROOT is set (or inferred from
6
+ * MAGENTO_CMS_PROJECT_RULES → project/.cursor/… → project root).
7
+ *
8
+ * Default: [0] = All Store Views.
9
+ * Override: pass store_ids from the tool args when the user names specific stores.
10
+ */
11
+
12
+ import { spawnSync } from 'node:child_process';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ export function resolveMagentoRoot() {
20
+ const fromEnv = String(process.env.MAGENTO_ROOT || '').trim();
21
+ if (fromEnv && fs.existsSync(path.join(fromEnv, 'app/bootstrap.php'))) {
22
+ return path.resolve(fromEnv);
23
+ }
24
+ const rules = String(process.env.MAGENTO_CMS_PROJECT_RULES || '').trim();
25
+ if (rules) {
26
+ const candidate = path.resolve(path.dirname(rules), '..');
27
+ if (fs.existsSync(path.join(candidate, 'app/bootstrap.php'))) {
28
+ return candidate;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * @param {object} args
36
+ * @returns {number[]}
37
+ */
38
+ export function resolveStoreIds(args = {}) {
39
+ let ids = [];
40
+ if (Array.isArray(args.store_ids) && args.store_ids.length) {
41
+ ids = args.store_ids.map((n) => Number(n)).filter((n) => Number.isFinite(n));
42
+ } else if (typeof args.store_ids === 'string' && args.store_ids.trim()) {
43
+ ids = args.store_ids
44
+ .split(',')
45
+ .map((s) => Number(s.trim()))
46
+ .filter((n) => Number.isFinite(n));
47
+ } else if (Array.isArray(args.store_codes) && args.store_codes.length) {
48
+ const codes = args.store_codes.map((c) => String(c).toLowerCase());
49
+ if (codes.includes('all') || codes.includes('*')) {
50
+ return [0];
51
+ }
52
+ // Numeric codes still accepted via store_ids; unknown codes → default all
53
+ return [0];
54
+ }
55
+
56
+ if (!ids.length) {
57
+ return [0];
58
+ }
59
+ if (ids.includes(0)) {
60
+ return [0];
61
+ }
62
+ return [...new Set(ids)];
63
+ }
64
+
65
+ /**
66
+ * @param {'page'|'block'} type
67
+ * @param {number} id
68
+ * @param {number[]} storeIds
69
+ */
70
+ export function assignCmsStores(type, id, storeIds = [0]) {
71
+ const root = resolveMagentoRoot();
72
+ if (!root) {
73
+ return {
74
+ ok: false,
75
+ skipped: true,
76
+ error:
77
+ 'Store assignment skipped: set MAGENTO_ROOT in mcp.json (path to Magento) so pages default to All Store Views (store_id 0).',
78
+ };
79
+ }
80
+
81
+ const srcScript = path.join(__dirname, '..', 'scripts', 'assign-stores.php');
82
+ if (!fs.existsSync(srcScript)) {
83
+ return { ok: false, error: `Missing ${srcScript}` };
84
+ }
85
+
86
+ const stores = (storeIds?.length ? storeIds : [0]).join(',');
87
+ const useDdev =
88
+ ['1', 'true', 'yes'].includes(
89
+ String(process.env.MAGENTO_USE_DDEV || '').toLowerCase()
90
+ ) || fs.existsSync(path.join(root, '.ddev'));
91
+
92
+ // Copy script into Magento var/ so ddev exec can run it inside the project mount
93
+ const destDir = path.join(root, 'var');
94
+ const destScript = path.join(destDir, 'mcp-assign-stores.php');
95
+ try {
96
+ fs.mkdirSync(destDir, { recursive: true });
97
+ fs.copyFileSync(srcScript, destScript);
98
+ } catch (e) {
99
+ return { ok: false, error: `Cannot stage assign script: ${e.message}` };
100
+ }
101
+
102
+ const args = [
103
+ 'var/mcp-assign-stores.php',
104
+ `--type=${type}`,
105
+ `--id=${id}`,
106
+ `--stores=${stores}`,
107
+ ];
108
+
109
+ let result;
110
+ if (useDdev) {
111
+ result = spawnSync('ddev', ['exec', 'php', ...args], {
112
+ cwd: root,
113
+ encoding: 'utf8',
114
+ timeout: 120000,
115
+ });
116
+ } else {
117
+ const phpBin = String(process.env.MAGENTO_PHP || 'php').trim() || 'php';
118
+ result = spawnSync(phpBin, [destScript, `--root=${root}`, ...args.slice(1)], {
119
+ encoding: 'utf8',
120
+ timeout: 120000,
121
+ env: process.env,
122
+ });
123
+ }
124
+
125
+ if (result.error) {
126
+ return { ok: false, error: result.error.message };
127
+ }
128
+ const out = String(result.stdout || '').trim();
129
+ const err = String(result.stderr || '').trim();
130
+ if (result.status !== 0) {
131
+ return { ok: false, error: err || out || `exit ${result.status}` };
132
+ }
133
+ try {
134
+ const jsonLine = out
135
+ .split('\n')
136
+ .map((l) => l.trim())
137
+ .filter(Boolean)
138
+ .pop();
139
+ const parsed = JSON.parse(jsonLine);
140
+ return {
141
+ ok: !!parsed.ok,
142
+ assigned_stores: parsed.assigned_stores,
143
+ requested_stores: parsed.requested_stores,
144
+ magento_root: root,
145
+ via: useDdev ? 'ddev' : 'php',
146
+ };
147
+ } catch {
148
+ return { ok: true, assigned_stores: storeIds, raw: out, magento_root: root };
149
+ }
150
+ }