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.
- package/.env.example +11 -0
- package/README.md +149 -0
- package/cursor-mcp.example.json +15 -0
- package/package.json +15 -0
- package/samples/figma-category.json +966 -0
- package/samples/figma-slider.json +900 -0
- package/samples/page-7.json +17 -0
- package/samples/page-8.json +15 -0
- package/scripts/create-demo-page.js +237 -0
- package/scripts/create-integration.php +80 -0
- package/scripts/create-melissa-home.js +287 -0
- package/scripts/health-check.js +18 -0
- package/scripts/smoke-e2e.js +42 -0
- package/scripts/test-compose.js +18 -0
- package/src/index.js +515 -0
- package/src/magentoClient.js +165 -0
- package/src/pageBuilder.js +108 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* End-to-end smoke test: creates a Page Builder CMS page via REST,
|
|
4
|
+
* reads it back, and prints the admin edit URL.
|
|
5
|
+
*
|
|
6
|
+
* NODE_EXTRA_CA_CERTS=~/.local/share/mkcert/rootCA.pem node scripts/smoke-e2e.js
|
|
7
|
+
*/
|
|
8
|
+
import { MagentoClient, loadDotEnv } from '../src/magentoClient.js';
|
|
9
|
+
import { composeFromFields, generateIdentifier, validatePageBuilderHtml } from '../src/pageBuilder.js';
|
|
10
|
+
|
|
11
|
+
loadDotEnv();
|
|
12
|
+
const client = MagentoClient.fromEnv();
|
|
13
|
+
|
|
14
|
+
const title = `MCP Smoke Test ${new Date().toISOString().slice(0, 16).replace('T', ' ')}`;
|
|
15
|
+
const content = composeFromFields({
|
|
16
|
+
headline: 'MCP Smoke Test',
|
|
17
|
+
subheadline: 'Created via mcp-magento-cms end-to-end test',
|
|
18
|
+
content: 'If you can see this page in Admin with Page Builder sections, the pipeline works.\nRow (contained) with a Text element.',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!validatePageBuilderHtml(content)) {
|
|
22
|
+
console.error('Composer output failed Page Builder validation');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const page = {
|
|
27
|
+
identifier: generateIdentifier(title),
|
|
28
|
+
title,
|
|
29
|
+
page_layout: '1column',
|
|
30
|
+
content_heading: '',
|
|
31
|
+
content,
|
|
32
|
+
active: true,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const created = await client.createPage(page);
|
|
36
|
+
console.log(`Created page id=${created.id} identifier=${created.identifier}`);
|
|
37
|
+
|
|
38
|
+
const readBack = await client.getPage(created.id);
|
|
39
|
+
const ok = validatePageBuilderHtml(readBack.content);
|
|
40
|
+
console.log(`Read-back content valid Page Builder HTML: ${ok}`);
|
|
41
|
+
console.log(`Admin edit URL: ${client.pageEditUrl(created.id)}`);
|
|
42
|
+
console.log(`Frontend URL: ${client.baseUrl}/${created.identifier}`);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { composeFromFields, validatePageBuilderHtml } from '../src/pageBuilder.js';
|
|
3
|
+
|
|
4
|
+
const html = composeFromFields({
|
|
5
|
+
content: 'Hello from MCP\nSecond line',
|
|
6
|
+
headline: 'Welcome',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
if (!validatePageBuilderHtml(html)) {
|
|
10
|
+
console.error('FAIL: invalid PB HTML');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
if (!html.includes('Welcome') || !html.includes('Hello from MCP')) {
|
|
14
|
+
console.error('FAIL: content missing');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
console.log('OK: Page Builder Row → Text composition');
|
|
18
|
+
console.log(html.slice(0, 200) + '...');
|
package/src/index.js
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Magento CMS MCP server (zero npm dependencies — Node 18+ only).
|
|
4
|
+
* Protocol: JSON-RPC 2.0 over stdio. Newline-delimited JSON by default
|
|
5
|
+
* (standard MCP stdio transport, used by Cursor); auto-switches to
|
|
6
|
+
* Content-Length framing if the client sends framed messages.
|
|
7
|
+
*
|
|
8
|
+
* Tools: magento_health, get/create/update cms page & block
|
|
9
|
+
* Page Builder: Row → Text
|
|
10
|
+
*
|
|
11
|
+
* Env: MAGENTO_BASE_URL, MAGENTO_ACCESS_TOKEN (or tools/mcp-magento-cms/.env)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import {
|
|
18
|
+
composeFromFields,
|
|
19
|
+
generateIdentifier,
|
|
20
|
+
validatePageBuilderHtml,
|
|
21
|
+
} from './pageBuilder.js';
|
|
22
|
+
import { MagentoClient, loadDotEnv } from './magentoClient.js';
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
loadDotEnv();
|
|
26
|
+
|
|
27
|
+
const SERVER_INFO = {
|
|
28
|
+
name: 'magento-cms',
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// JSON-RPC / MCP framing
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
let clientUsesContentLength = false;
|
|
37
|
+
|
|
38
|
+
function sendMessage(msg) {
|
|
39
|
+
const json = JSON.stringify(msg);
|
|
40
|
+
if (clientUsesContentLength) {
|
|
41
|
+
const body = Buffer.from(json, 'utf8');
|
|
42
|
+
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
|
|
43
|
+
process.stdout.write(body);
|
|
44
|
+
} else {
|
|
45
|
+
process.stdout.write(json + '\n');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sendResult(id, result) {
|
|
50
|
+
sendMessage({ jsonrpc: '2.0', id, result });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sendError(id, code, message) {
|
|
54
|
+
sendMessage({
|
|
55
|
+
jsonrpc: '2.0',
|
|
56
|
+
id,
|
|
57
|
+
error: { code, message },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Tool result helpers (MCP content items) */
|
|
62
|
+
function toolOk(data) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: typeof data === 'string' ? data : JSON.stringify(data, null, 2),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toolFail(err) {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Magento tools
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function client() {
|
|
86
|
+
return MagentoClient.fromEnv();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function summarizeEntity(entity, type, editUrl) {
|
|
90
|
+
return {
|
|
91
|
+
type,
|
|
92
|
+
id: entity?.id ?? entity?.page_id ?? entity?.block_id ?? null,
|
|
93
|
+
title: entity?.title ?? null,
|
|
94
|
+
identifier: entity?.identifier ?? null,
|
|
95
|
+
is_active: entity?.is_active ?? null,
|
|
96
|
+
content_preview: String(entity?.content || '').slice(0, 280),
|
|
97
|
+
has_pagebuilder:
|
|
98
|
+
typeof entity?.content === 'string' &&
|
|
99
|
+
entity.content.includes('data-content-type="row"'),
|
|
100
|
+
admin_edit_url: editUrl,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const TOOLS = [
|
|
105
|
+
{
|
|
106
|
+
name: 'magento_health',
|
|
107
|
+
description:
|
|
108
|
+
'Check Magento REST connectivity and Integration token (reads CMS pages list).',
|
|
109
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'get_cms_page',
|
|
113
|
+
description: 'Get a Magento CMS page by page_id (numeric id).',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
page_id: { type: 'number', description: 'CMS page ID, e.g. 8' },
|
|
118
|
+
},
|
|
119
|
+
required: ['page_id'],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'create_cms_page',
|
|
124
|
+
description:
|
|
125
|
+
'Create a Magento CMS page with Page Builder content (Row → Text) from simple text.',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
title: { type: 'string', description: 'Page title' },
|
|
130
|
+
content: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'Plain text or simple HTML body',
|
|
133
|
+
},
|
|
134
|
+
identifier: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
description: 'URL key; auto from title if omitted',
|
|
137
|
+
},
|
|
138
|
+
headline: { type: 'string', description: 'Optional H2 above body' },
|
|
139
|
+
is_active: { type: 'boolean', description: 'Default true' },
|
|
140
|
+
page_layout: { type: 'string', description: 'Default 1column' },
|
|
141
|
+
},
|
|
142
|
+
required: ['title', 'content'],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'update_cms_page',
|
|
147
|
+
description:
|
|
148
|
+
'Update an existing Magento CMS page by page_id. Rebuilds Page Builder Row → Text when content is provided.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
page_id: { type: 'number', description: 'CMS page ID to update' },
|
|
153
|
+
title: { type: 'string' },
|
|
154
|
+
content: { type: 'string' },
|
|
155
|
+
headline: { type: 'string' },
|
|
156
|
+
identifier: { type: 'string' },
|
|
157
|
+
is_active: { type: 'boolean' },
|
|
158
|
+
page_layout: { type: 'string' },
|
|
159
|
+
},
|
|
160
|
+
required: ['page_id'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'get_cms_block',
|
|
165
|
+
description: 'Get a Magento CMS block by block_id.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
block_id: { type: 'number' },
|
|
170
|
+
},
|
|
171
|
+
required: ['block_id'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'create_cms_block',
|
|
176
|
+
description:
|
|
177
|
+
'Create a Magento CMS block with Page Builder Row → Text from simple text.',
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
title: { type: 'string' },
|
|
182
|
+
content: { type: 'string' },
|
|
183
|
+
identifier: { type: 'string' },
|
|
184
|
+
headline: { type: 'string' },
|
|
185
|
+
is_active: { type: 'boolean' },
|
|
186
|
+
},
|
|
187
|
+
required: ['title', 'content'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'update_cms_block',
|
|
192
|
+
description:
|
|
193
|
+
'Update Magento CMS block by block_id. Rebuilds Page Builder when content is set.',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
block_id: { type: 'number' },
|
|
198
|
+
title: { type: 'string' },
|
|
199
|
+
content: { type: 'string' },
|
|
200
|
+
headline: { type: 'string' },
|
|
201
|
+
identifier: { type: 'string' },
|
|
202
|
+
is_active: { type: 'boolean' },
|
|
203
|
+
},
|
|
204
|
+
required: ['block_id'],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'list_section_types',
|
|
209
|
+
description:
|
|
210
|
+
'List supported Page Builder section types (MVP: simple_text).',
|
|
211
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
async function callTool(name, args = {}) {
|
|
216
|
+
switch (name) {
|
|
217
|
+
case 'magento_health': {
|
|
218
|
+
const magento = client();
|
|
219
|
+
const result = await magento.health();
|
|
220
|
+
return toolOk({
|
|
221
|
+
...result,
|
|
222
|
+
message: 'Magento REST OK. You can create/update CMS pages and blocks.',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
case 'get_cms_page': {
|
|
226
|
+
const magento = client();
|
|
227
|
+
const page = await magento.getPage(args.page_id);
|
|
228
|
+
return toolOk(
|
|
229
|
+
summarizeEntity(page, 'page', magento.pageEditUrl(page.id || args.page_id))
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
case 'create_cms_page': {
|
|
233
|
+
const magento = client();
|
|
234
|
+
const identifier =
|
|
235
|
+
(args.identifier || '').trim() || generateIdentifier(args.title);
|
|
236
|
+
const pbHtml = composeFromFields({
|
|
237
|
+
content: args.content,
|
|
238
|
+
headline: args.headline,
|
|
239
|
+
});
|
|
240
|
+
if (!validatePageBuilderHtml(pbHtml)) {
|
|
241
|
+
throw new Error('Generated Page Builder HTML failed validation');
|
|
242
|
+
}
|
|
243
|
+
const page = await magento.createPage({
|
|
244
|
+
title: args.title,
|
|
245
|
+
identifier,
|
|
246
|
+
content: pbHtml,
|
|
247
|
+
content_heading: '',
|
|
248
|
+
page_layout: args.page_layout || '1column',
|
|
249
|
+
is_active: args.is_active !== false,
|
|
250
|
+
store_id: [0],
|
|
251
|
+
meta_title: args.title,
|
|
252
|
+
meta_description: '',
|
|
253
|
+
});
|
|
254
|
+
const id = page?.id ?? page?.page_id;
|
|
255
|
+
return toolOk({
|
|
256
|
+
result: 'created',
|
|
257
|
+
...summarizeEntity(page, 'page', magento.pageEditUrl(id)),
|
|
258
|
+
full_content_length: pbHtml.length,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
case 'update_cms_page': {
|
|
262
|
+
const magento = client();
|
|
263
|
+
const existing = await magento.getPage(args.page_id);
|
|
264
|
+
const title = (args.title || '').trim() || existing.title;
|
|
265
|
+
const identifier =
|
|
266
|
+
(args.identifier || '').trim() ||
|
|
267
|
+
existing.identifier ||
|
|
268
|
+
generateIdentifier(title);
|
|
269
|
+
|
|
270
|
+
let content = existing.content;
|
|
271
|
+
if (args.content !== undefined && String(args.content || '').length) {
|
|
272
|
+
content = composeFromFields({
|
|
273
|
+
content: args.content,
|
|
274
|
+
headline: args.headline,
|
|
275
|
+
});
|
|
276
|
+
if (!validatePageBuilderHtml(content)) {
|
|
277
|
+
throw new Error('Generated Page Builder HTML failed validation');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const page = await magento.updatePage(args.page_id, {
|
|
282
|
+
title,
|
|
283
|
+
identifier,
|
|
284
|
+
content,
|
|
285
|
+
content_heading: existing.content_heading ?? '',
|
|
286
|
+
page_layout: args.page_layout || existing.page_layout || '1column',
|
|
287
|
+
is_active:
|
|
288
|
+
args.is_active !== undefined ? args.is_active : existing.is_active,
|
|
289
|
+
meta_title: existing.meta_title || title,
|
|
290
|
+
meta_description: existing.meta_description || '',
|
|
291
|
+
store_id: existing.store_id || [0],
|
|
292
|
+
});
|
|
293
|
+
const id = page?.id ?? args.page_id;
|
|
294
|
+
return toolOk({
|
|
295
|
+
result: 'updated',
|
|
296
|
+
...summarizeEntity(page, 'page', magento.pageEditUrl(id)),
|
|
297
|
+
full_content_length: String(content || '').length,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
case 'get_cms_block': {
|
|
301
|
+
const magento = client();
|
|
302
|
+
const block = await magento.getBlock(args.block_id);
|
|
303
|
+
return toolOk(
|
|
304
|
+
summarizeEntity(block, 'block', magento.blockEditUrl(block.id || args.block_id))
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
case 'create_cms_block': {
|
|
308
|
+
const magento = client();
|
|
309
|
+
const identifier =
|
|
310
|
+
(args.identifier || '').trim() || generateIdentifier(args.title);
|
|
311
|
+
const pbHtml = composeFromFields({
|
|
312
|
+
content: args.content,
|
|
313
|
+
headline: args.headline,
|
|
314
|
+
});
|
|
315
|
+
if (!validatePageBuilderHtml(pbHtml)) {
|
|
316
|
+
throw new Error('Generated Page Builder HTML failed validation');
|
|
317
|
+
}
|
|
318
|
+
const block = await magento.createBlock({
|
|
319
|
+
title: args.title,
|
|
320
|
+
identifier,
|
|
321
|
+
content: pbHtml,
|
|
322
|
+
is_active: args.is_active !== false,
|
|
323
|
+
store_id: [0],
|
|
324
|
+
});
|
|
325
|
+
const id = block?.id ?? block?.block_id;
|
|
326
|
+
return toolOk({
|
|
327
|
+
result: 'created',
|
|
328
|
+
...summarizeEntity(block, 'block', magento.blockEditUrl(id)),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
case 'update_cms_block': {
|
|
332
|
+
const magento = client();
|
|
333
|
+
const existing = await magento.getBlock(args.block_id);
|
|
334
|
+
const title = (args.title || '').trim() || existing.title;
|
|
335
|
+
const identifier =
|
|
336
|
+
(args.identifier || '').trim() ||
|
|
337
|
+
existing.identifier ||
|
|
338
|
+
generateIdentifier(title);
|
|
339
|
+
|
|
340
|
+
let content = existing.content;
|
|
341
|
+
if (args.content !== undefined && String(args.content || '').length) {
|
|
342
|
+
content = composeFromFields({
|
|
343
|
+
content: args.content,
|
|
344
|
+
headline: args.headline,
|
|
345
|
+
});
|
|
346
|
+
if (!validatePageBuilderHtml(content)) {
|
|
347
|
+
throw new Error('Generated Page Builder HTML failed validation');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const block = await magento.updateBlock(args.block_id, {
|
|
352
|
+
title,
|
|
353
|
+
identifier,
|
|
354
|
+
content,
|
|
355
|
+
is_active:
|
|
356
|
+
args.is_active !== undefined ? args.is_active : existing.is_active,
|
|
357
|
+
store_id: existing.store_id || [0],
|
|
358
|
+
});
|
|
359
|
+
const id = block?.id ?? args.block_id;
|
|
360
|
+
return toolOk({
|
|
361
|
+
result: 'updated',
|
|
362
|
+
...summarizeEntity(block, 'block', magento.blockEditUrl(id)),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
case 'list_section_types':
|
|
366
|
+
return toolOk({
|
|
367
|
+
section_types: [
|
|
368
|
+
{
|
|
369
|
+
type: 'simple_text',
|
|
370
|
+
description:
|
|
371
|
+
'Page Builder Row (contained) → Text (WYSIWYG). Default for pasted content.',
|
|
372
|
+
params: ['content', 'headline?', 'subheadline?'],
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
note: 'More types will be added as element templates are documented.',
|
|
376
|
+
});
|
|
377
|
+
default:
|
|
378
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Request handlers
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
async function handleRequest(msg) {
|
|
387
|
+
const { id, method, params } = msg;
|
|
388
|
+
|
|
389
|
+
// Notifications have no id
|
|
390
|
+
if (id === undefined || id === null) {
|
|
391
|
+
if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// ignore other notifications
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
switch (method) {
|
|
400
|
+
case 'initialize':
|
|
401
|
+
sendResult(id, {
|
|
402
|
+
protocolVersion: params?.protocolVersion || '2024-11-05',
|
|
403
|
+
capabilities: {
|
|
404
|
+
tools: {},
|
|
405
|
+
},
|
|
406
|
+
serverInfo: SERVER_INFO,
|
|
407
|
+
});
|
|
408
|
+
break;
|
|
409
|
+
|
|
410
|
+
case 'ping':
|
|
411
|
+
sendResult(id, {});
|
|
412
|
+
break;
|
|
413
|
+
|
|
414
|
+
case 'tools/list':
|
|
415
|
+
sendResult(id, { tools: TOOLS });
|
|
416
|
+
break;
|
|
417
|
+
|
|
418
|
+
case 'tools/call': {
|
|
419
|
+
const name = params?.name;
|
|
420
|
+
const args = params?.arguments || {};
|
|
421
|
+
try {
|
|
422
|
+
const result = await callTool(name, args);
|
|
423
|
+
sendResult(id, result);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
sendResult(id, toolFail(e));
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
default:
|
|
431
|
+
sendError(id, -32601, `Method not found: ${method}`);
|
|
432
|
+
}
|
|
433
|
+
} catch (e) {
|
|
434
|
+
sendError(id, -32603, e instanceof Error ? e.message : String(e));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// Stdio reader (Content-Length framing)
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
let buffer = Buffer.alloc(0);
|
|
443
|
+
|
|
444
|
+
process.stdin.on('data', (chunk) => {
|
|
445
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
446
|
+
processBuffer();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
process.stdin.on('error', (err) => {
|
|
450
|
+
console.error('stdin error', err);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
function processBuffer() {
|
|
454
|
+
while (true) {
|
|
455
|
+
// Skip leading whitespace/newlines between messages
|
|
456
|
+
let start = 0;
|
|
457
|
+
while (
|
|
458
|
+
start < buffer.length &&
|
|
459
|
+
(buffer[start] === 0x0a || buffer[start] === 0x0d || buffer[start] === 0x20 || buffer[start] === 0x09)
|
|
460
|
+
) {
|
|
461
|
+
start++;
|
|
462
|
+
}
|
|
463
|
+
if (start > 0) buffer = buffer.slice(start);
|
|
464
|
+
if (buffer.length === 0) return;
|
|
465
|
+
|
|
466
|
+
if (buffer[0] === 0x7b /* '{' */) {
|
|
467
|
+
// Newline-delimited JSON (standard MCP stdio, used by Cursor)
|
|
468
|
+
const nl = buffer.indexOf('\n');
|
|
469
|
+
if (nl === -1) return;
|
|
470
|
+
const line = buffer.slice(0, nl).toString('utf8').trim();
|
|
471
|
+
buffer = buffer.slice(nl + 1);
|
|
472
|
+
if (!line) continue;
|
|
473
|
+
try {
|
|
474
|
+
const msg = JSON.parse(line);
|
|
475
|
+
void handleRequest(msg);
|
|
476
|
+
} catch (e) {
|
|
477
|
+
console.error('parse error', e);
|
|
478
|
+
}
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Content-Length framed (LSP-style) message
|
|
483
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
484
|
+
if (headerEnd === -1) return;
|
|
485
|
+
|
|
486
|
+
const header = buffer.slice(0, headerEnd).toString('utf8');
|
|
487
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
488
|
+
if (!match) {
|
|
489
|
+
// Unrecognized header block; drop it to avoid an infinite loop
|
|
490
|
+
buffer = buffer.slice(headerEnd + 4);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const length = parseInt(match[1], 10);
|
|
495
|
+
const bodyStart = headerEnd + 4;
|
|
496
|
+
if (buffer.length < bodyStart + length) return;
|
|
497
|
+
|
|
498
|
+
const body = buffer.slice(bodyStart, bodyStart + length).toString('utf8');
|
|
499
|
+
buffer = buffer.slice(bodyStart + length);
|
|
500
|
+
clientUsesContentLength = true;
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const msg = JSON.parse(body);
|
|
504
|
+
void handleRequest(msg);
|
|
505
|
+
} catch (e) {
|
|
506
|
+
console.error('parse error', e);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Keep process alive
|
|
512
|
+
process.stdin.resume();
|
|
513
|
+
|
|
514
|
+
// Log to stderr only (stdout is MCP protocol)
|
|
515
|
+
console.error(`[magento-cms MCP] ready (base=${process.env.MAGENTO_BASE_URL || 'unset'})`);
|