mcp-magento-cms 1.0.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.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Magento REST client (Integration Access Token as Bearer).
3
+ */
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ export function loadDotEnv() {
12
+ const envPath = path.join(__dirname, '..', '.env');
13
+ if (!fs.existsSync(envPath)) return;
14
+ const text = fs.readFileSync(envPath, 'utf8');
15
+ for (const line of text.split('\n')) {
16
+ const trimmed = line.trim();
17
+ if (!trimmed || trimmed.startsWith('#')) continue;
18
+ const eq = trimmed.indexOf('=');
19
+ if (eq === -1) continue;
20
+ const key = trimmed.slice(0, eq).trim();
21
+ let val = trimmed.slice(eq + 1).trim();
22
+ if (
23
+ (val.startsWith('"') && val.endsWith('"')) ||
24
+ (val.startsWith("'") && val.endsWith("'"))
25
+ ) {
26
+ val = val.slice(1, -1);
27
+ }
28
+ if (process.env[key] === undefined) {
29
+ process.env[key] = val;
30
+ }
31
+ }
32
+ }
33
+
34
+ export class MagentoClient {
35
+ constructor({ baseUrl, token, storeCode = '', adminPath = 'admin' } = {}) {
36
+ this.baseUrl = String(baseUrl || '').replace(/\/+$/, '');
37
+ this.token = String(token || '').trim();
38
+ this.storeCode = String(storeCode || '').trim();
39
+ this.adminPath = String(adminPath || 'admin').replace(/^\/+|\/+$/g, '');
40
+ }
41
+
42
+ static fromEnv() {
43
+ loadDotEnv();
44
+ return new MagentoClient({
45
+ baseUrl: process.env.MAGENTO_BASE_URL,
46
+ token: process.env.MAGENTO_ACCESS_TOKEN,
47
+ storeCode: process.env.MAGENTO_STORE_CODE || '',
48
+ adminPath: process.env.MAGENTO_ADMIN_PATH || 'admin',
49
+ });
50
+ }
51
+
52
+ ensureConfig() {
53
+ if (!this.baseUrl) {
54
+ throw new Error('MAGENTO_BASE_URL is not set');
55
+ }
56
+ if (!this.token) {
57
+ throw new Error('MAGENTO_ACCESS_TOKEN is not set');
58
+ }
59
+ }
60
+
61
+ restPath(apiPath) {
62
+ let p = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
63
+ // Normalize to path after /V1
64
+ if (p.startsWith('/V1')) {
65
+ p = p.slice(3);
66
+ }
67
+ if (!p.startsWith('/')) {
68
+ p = `/${p}`;
69
+ }
70
+ if (this.storeCode) {
71
+ return `${this.baseUrl}/rest/${this.storeCode}/V1${p}`;
72
+ }
73
+ return `${this.baseUrl}/rest/V1${p}`;
74
+ }
75
+
76
+ async request(method, apiPath, body) {
77
+ this.ensureConfig();
78
+ const url = this.restPath(apiPath);
79
+ const headers = {
80
+ Authorization: `Bearer ${this.token}`,
81
+ Accept: 'application/json',
82
+ 'Content-Type': 'application/json',
83
+ };
84
+ const res = await fetch(url, {
85
+ method,
86
+ headers,
87
+ body: body !== undefined ? JSON.stringify(body) : undefined,
88
+ });
89
+ const text = await res.text();
90
+ let data;
91
+ try {
92
+ data = text ? JSON.parse(text) : null;
93
+ } catch {
94
+ data = text;
95
+ }
96
+ if (!res.ok) {
97
+ const msg =
98
+ typeof data === 'object' && data?.message
99
+ ? data.message
100
+ : typeof data === 'string'
101
+ ? data
102
+ : res.statusText;
103
+ throw new Error(`Magento ${method} ${url} failed (${res.status}): ${msg}`);
104
+ }
105
+ return data;
106
+ }
107
+
108
+ async get(apiPath) {
109
+ return this.request('GET', apiPath);
110
+ }
111
+
112
+ async post(apiPath, body) {
113
+ return this.request('POST', apiPath, body);
114
+ }
115
+
116
+ async put(apiPath, body) {
117
+ return this.request('PUT', apiPath, body);
118
+ }
119
+
120
+ pageEditUrl(pageId) {
121
+ return `${this.baseUrl}/${this.adminPath}/cms/page/edit/page_id/${pageId}`;
122
+ }
123
+
124
+ blockEditUrl(blockId) {
125
+ return `${this.baseUrl}/${this.adminPath}/cms/block/edit/block_id/${blockId}`;
126
+ }
127
+
128
+ async health() {
129
+ this.ensureConfig();
130
+ const result = await this.get(
131
+ '/cmsPage/search?searchCriteria[pageSize]=1&searchCriteria[currentPage]=1'
132
+ );
133
+ return {
134
+ ok: true,
135
+ baseUrl: this.baseUrl,
136
+ totalPagesSample: result?.total_count ?? null,
137
+ };
138
+ }
139
+
140
+ async getPage(pageId) {
141
+ return this.get(`/cmsPage/${pageId}`);
142
+ }
143
+
144
+ async createPage(page) {
145
+ return this.post('/cmsPage', { page });
146
+ }
147
+
148
+ async updatePage(pageId, page) {
149
+ return this.put(`/cmsPage/${pageId}`, { page: { ...page, id: Number(pageId) } });
150
+ }
151
+
152
+ async getBlock(blockId) {
153
+ return this.get(`/cmsBlock/${blockId}`);
154
+ }
155
+
156
+ async createBlock(block) {
157
+ return this.post('/cmsBlock', { block });
158
+ }
159
+
160
+ async updateBlock(blockId, block) {
161
+ return this.put(`/cmsBlock/${blockId}`, {
162
+ block: { ...block, id: Number(blockId) },
163
+ });
164
+ }
165
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Page Builder simple text: Row (contained) → Text
3
+ * Matches Brainvire_CmsContentStudio SimpleTextComposer.
4
+ */
5
+
6
+ function escapeHtml(value) {
7
+ return String(value)
8
+ .replace(/&/g, '&')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&#039;');
13
+ }
14
+
15
+ function looksLikeHtml(value) {
16
+ return /<[^>]+>/.test(value);
17
+ }
18
+
19
+ function plainTextToParagraphs(text) {
20
+ const parts = String(text)
21
+ .split(/\r\n|\r|\n/)
22
+ .map((l) => l.trim())
23
+ .filter(Boolean)
24
+ .map((l) => `<p>${escapeHtml(l)}</p>`);
25
+ return parts.length ? parts.join('') : '<p></p>';
26
+ }
27
+
28
+ function normalizeInnerHtml(htmlContent) {
29
+ const t = String(htmlContent || '').trim();
30
+ if (!t) return '<p></p>';
31
+ if (!looksLikeHtml(t)) return plainTextToParagraphs(t);
32
+ return t;
33
+ }
34
+
35
+ function generateStyleId(prefix) {
36
+ const random = [...Array(12)]
37
+ .map(() => Math.floor(Math.random() * 16).toString(16))
38
+ .join('');
39
+ return `${prefix}${random}`.toUpperCase();
40
+ }
41
+
42
+ /**
43
+ * @param {string} htmlContent
44
+ * @returns {string}
45
+ */
46
+ export function composePageBuilderSimpleText(htmlContent) {
47
+ const inner = normalizeInnerHtml(htmlContent);
48
+ const rowStyleId = generateStyleId('R');
49
+ const textStyleId = generateStyleId('T');
50
+
51
+ return (
52
+ `<div data-content-type="row" data-appearance="contained" data-element="main">` +
53
+ `<div data-enable-parallax="0" data-parallax-speed="0.5" data-background-images="{}"` +
54
+ ` data-background-type="image" data-video-loop="true" data-video-play-only-visible="true"` +
55
+ ` data-video-lazy-load="true" data-video-fallback-src="" data-element="inner"` +
56
+ ` data-pb-style="${rowStyleId}"` +
57
+ ` style="justify-content: flex-start; display: flex; flex-direction: column;` +
58
+ ` background-position: left top; background-size: cover; background-repeat: no-repeat;` +
59
+ ` background-attachment: scroll; border-style: none; border-width: 1px; border-radius: 0px;` +
60
+ ` margin: 0px 0px 10px; padding: 10px;">` +
61
+ `<div data-content-type="text" data-appearance="default" data-element="main"` +
62
+ ` data-pb-style="${textStyleId}" class="bypass-html-filter"` +
63
+ ` style="border-style: none; border-width: 1px; border-radius: 0px; margin: 0px; padding: 0px;">` +
64
+ `${inner}` +
65
+ `</div></div></div>` +
66
+ `<style>` +
67
+ `#html-body [data-pb-style=${rowStyleId}]{justify-content:flex-start;display:flex;flex-direction:column;` +
68
+ `background-position:left top;background-size:cover;background-repeat:no-repeat;` +
69
+ `background-attachment:scroll}` +
70
+ `#html-body [data-pb-style=${textStyleId}]{border-style:none}` +
71
+ `</style>`
72
+ );
73
+ }
74
+
75
+ /**
76
+ * @param {{ content?: string, headline?: string, subheadline?: string }} fields
77
+ */
78
+ export function composeFromFields(fields = {}) {
79
+ const chunks = [];
80
+ const headline = (fields.headline || '').trim();
81
+ const subheadline = (fields.subheadline || '').trim();
82
+ const content = (fields.content || fields.source_content || '').trim();
83
+
84
+ if (headline) chunks.push(`<h2>${escapeHtml(headline)}</h2>`);
85
+ if (subheadline) chunks.push(`<p><strong>${escapeHtml(subheadline)}</strong></p>`);
86
+ if (content) {
87
+ chunks.push(looksLikeHtml(content) ? content : plainTextToParagraphs(content));
88
+ }
89
+
90
+ return composePageBuilderSimpleText(chunks.length ? chunks.join('') : '<p></p>');
91
+ }
92
+
93
+ export function generateIdentifier(title) {
94
+ const value = String(title || '')
95
+ .toLowerCase()
96
+ .trim()
97
+ .replace(/[^a-z0-9]+/g, '-')
98
+ .replace(/^-+|-+$/g, '');
99
+ return value || `page-${Date.now()}`;
100
+ }
101
+
102
+ export function validatePageBuilderHtml(html) {
103
+ return (
104
+ typeof html === 'string' &&
105
+ html.includes('data-content-type="row"') &&
106
+ html.includes('data-content-type="text"')
107
+ );
108
+ }