mcp-server-sfmc 1.9.2 → 2.1.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 +58 -8
- package/dist/cli/reviewDiff.d.ts +1 -17
- package/dist/cli/reviewDiff.d.ts.map +1 -1
- package/dist/cli/reviewDiff.js +25 -53
- package/dist/cli/reviewDiff.js.map +1 -1
- package/dist/cli/reviewSeverity.d.ts +23 -0
- package/dist/cli/reviewSeverity.d.ts.map +1 -0
- package/dist/cli/reviewSeverity.js +36 -0
- package/dist/cli/reviewSeverity.js.map +1 -0
- package/dist/conversion-rules.d.ts +72 -0
- package/dist/conversion-rules.d.ts.map +1 -1
- package/dist/conversion-rules.js +544 -165
- package/dist/conversion-rules.js.map +1 -1
- package/dist/index.js +729 -129
- package/dist/index.js.map +1 -1
- package/dist/mce-help-search.js +5 -5
- package/dist/mce-help-search.js.map +1 -1
- package/dist/mcn-help-search.js +5 -5
- package/dist/mcn-help-search.js.map +1 -1
- package/package.json +23 -10
package/dist/index.js
CHANGED
|
@@ -16,12 +16,26 @@ import { z } from 'zod';
|
|
|
16
16
|
import { sfmcLanguageService, validateAmpscript, validateSsjs, validateGtlBlocks, isMcnSupported, getMcnApiVersion, getMcnNotes, extractAmpscriptFunctionCalls, } from 'sfmc-language-lsp';
|
|
17
17
|
import { getChunks, getMceHelpStats, searchMceHelp, } from './mce-help-search.js';
|
|
18
18
|
import { getMcnChunks, getMcnHelpStats, searchMcnHelp } from './mcn-help-search.js';
|
|
19
|
-
import { CLOUDPAGES_ONLY_FUNCTIONS, NON_MIGRATABLE_SSJS_PATTERNS, ssjsToAmpscript, ampscriptToSsjs, rewriteAmpForMcn, isSsjsBlockConvertible, } from './conversion-rules.js';
|
|
19
|
+
import { CLOUDPAGES_ONLY_FUNCTIONS, NON_MIGRATABLE_SSJS_PATTERNS, ssjsToAmpscript, ampscriptToSsjs, rewriteAmpForMcn, isSsjsBlockConvertible, ampscriptToHandlebars, handlebarsToAmpscript, ssjsToHandlebars, AMP_MCN_HANDLEBARS_GAP, HBS_GAP_NOTE, } from './conversion-rules.js';
|
|
20
20
|
import { ECMASCRIPT_BUILTINS, KNOWN_UNSUPPORTED, polyfillByPrototypeName, polyfillByStaticName, } from 'ssjs-data';
|
|
21
21
|
function projectPackageRoot() {
|
|
22
22
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const packageJsonPath = path.join(projectPackageRoot(), 'package.json');
|
|
25
|
+
const package_ = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
26
|
+
/**
|
|
27
|
+
* Normalize a completion item's `label` (which may be a string or a
|
|
28
|
+
* `{ label: string }` object) to its plain-string form.
|
|
29
|
+
* @param item - A completion item with a string or object label.
|
|
30
|
+
* @param item.label
|
|
31
|
+
* @returns {string} The label text.
|
|
32
|
+
*/
|
|
33
|
+
function labelText(item) {
|
|
34
|
+
if (typeof item.label === 'string') {
|
|
35
|
+
return item.label;
|
|
36
|
+
}
|
|
37
|
+
return item.label.label;
|
|
38
|
+
}
|
|
25
39
|
// ---------------------------------------------------------------------------
|
|
26
40
|
// Server instance
|
|
27
41
|
// ---------------------------------------------------------------------------
|
|
@@ -84,7 +98,7 @@ const SERVER_INSTRUCTIONS = 'This server provides authoritative SFMC language in
|
|
|
84
98
|
'and route the query to the right doc index (or both for MCN). Only fall back to the individual ' +
|
|
85
99
|
'search tools when you need to scope by `product_focus` (MCE only) or when you are **certain** ' +
|
|
86
100
|
'which doc index to target.';
|
|
87
|
-
const server = new McpServer({ name: 'mcp-server-sfmc', version:
|
|
101
|
+
const server = new McpServer({ name: 'mcp-server-sfmc', version: package_.version }, { instructions: SERVER_INSTRUCTIONS });
|
|
88
102
|
function detectLanguage(code, hint) {
|
|
89
103
|
if (hint === 'ssjs')
|
|
90
104
|
return 'ssjs';
|
|
@@ -106,6 +120,17 @@ function detectLanguage(code, hint) {
|
|
|
106
120
|
return 'ssjs';
|
|
107
121
|
return 'ampscript';
|
|
108
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Extracts the human-readable text from a diagnostic message, which may be a
|
|
125
|
+
* plain string or a `{ value }` wrapper depending on the language-service type.
|
|
126
|
+
* @param message
|
|
127
|
+
*/
|
|
128
|
+
function diagnosticMessage(message) {
|
|
129
|
+
if (typeof message === 'string') {
|
|
130
|
+
return message;
|
|
131
|
+
}
|
|
132
|
+
return message.value;
|
|
133
|
+
}
|
|
109
134
|
function formatDiagnostics(diagnostics) {
|
|
110
135
|
if (diagnostics.length === 0)
|
|
111
136
|
return 'No issues found.';
|
|
@@ -113,11 +138,33 @@ function formatDiagnostics(diagnostics) {
|
|
|
113
138
|
.map((d) => {
|
|
114
139
|
const sev = d.severity === 1 ? 'ERROR' : d.severity === 2 ? 'WARNING' : 'INFO';
|
|
115
140
|
const loc = `line ${d.range.start.line + 1}, col ${d.range.start.character + 1}`;
|
|
116
|
-
const message =
|
|
141
|
+
const message = diagnosticMessage(d.message);
|
|
117
142
|
return `[${sev}] ${loc}: ${message}`;
|
|
118
143
|
})
|
|
119
144
|
.join('\n');
|
|
120
145
|
}
|
|
146
|
+
function formatHandlebarsHelper(helper) {
|
|
147
|
+
const parameters = helper.params
|
|
148
|
+
.map((p) => {
|
|
149
|
+
const request = p.optional ? '(optional)' : '(required)';
|
|
150
|
+
const variadic = p.variadic ? ' (variadic)' : '';
|
|
151
|
+
return ` - ${p.name}: ${p.type}${variadic} ${request}${p.description ? ' — ' + p.description : ''}`;
|
|
152
|
+
})
|
|
153
|
+
.join('\n');
|
|
154
|
+
const subexpr = helper.subexpressionOnly
|
|
155
|
+
? '\n\n> Subexpression-only — may only be used inside `( … )`.'
|
|
156
|
+
: '';
|
|
157
|
+
return (`## {{${helper.name}}}\n\n` +
|
|
158
|
+
`**Category:** ${helper.category}\n\n` +
|
|
159
|
+
`**Origin:** ${helper.origin}\n\n` +
|
|
160
|
+
`**Type:** ${helper.helperType}\n\n` +
|
|
161
|
+
`**Returns:** ${helper.returnType}\n\n` +
|
|
162
|
+
`**Description:** ${helper.description}\n\n` +
|
|
163
|
+
`**Parameters:**\n${parameters || ' (none)'}` +
|
|
164
|
+
subexpr +
|
|
165
|
+
`\n\n✅ **Marketing Cloud Next:** Supported since API v${helper.mcnSince}.0` +
|
|
166
|
+
(helper.docUrl ? `\n\n[Documentation](${helper.docUrl})` : ''));
|
|
167
|
+
}
|
|
121
168
|
// ---------------------------------------------------------------------------
|
|
122
169
|
// Tool: validate_ampscript
|
|
123
170
|
// ---------------------------------------------------------------------------
|
|
@@ -215,28 +262,28 @@ server.tool('lookup_ampscript_function', 'Look up the signature, parameters, des
|
|
|
215
262
|
'Case-insensitive. Returns null if the function is not found.', {
|
|
216
263
|
name: z.string().describe('The AMPscript function name, e.g. "Lookup", "DateAdd", "IIf".'),
|
|
217
264
|
}, ({ name }) => {
|
|
218
|
-
const
|
|
219
|
-
if (!
|
|
265
|
+
const function_ = sfmcLanguageService.lookupAmpscriptFunction(name);
|
|
266
|
+
if (!function_) {
|
|
220
267
|
return { content: [{ type: 'text', text: `AMPscript function "${name}" not found.` }] };
|
|
221
268
|
}
|
|
222
|
-
const
|
|
269
|
+
const parameters = function_.params
|
|
223
270
|
.map((p) => {
|
|
224
|
-
const
|
|
225
|
-
return ` - ${p.name}: ${p.type ?? 'any'} ${
|
|
271
|
+
const request = p.optional ? '(optional)' : '(required)';
|
|
272
|
+
return ` - ${p.name}: ${p.type ?? 'any'} ${request}${p.description ? ' — ' + p.description : ''}`;
|
|
226
273
|
})
|
|
227
274
|
.join('\n');
|
|
228
|
-
const examples =
|
|
275
|
+
const examples = function_.example ? '\n\nExample:\n' + function_.example : '';
|
|
229
276
|
// MCN compatibility badge
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
const mcnLine =
|
|
277
|
+
const functionMcnSince = function_.mcnSince ?? null;
|
|
278
|
+
const functionMcnNotes = function_.mcnNotes ?? null;
|
|
279
|
+
const mcnLine = functionMcnSince === null
|
|
233
280
|
? '\n\n❌ **Marketing Cloud Next:** Not supported'
|
|
234
|
-
: `\n\n✅ **Marketing Cloud Next:** Supported since API v${
|
|
235
|
-
(
|
|
236
|
-
const text = `## ${
|
|
237
|
-
`**Category:** ${
|
|
238
|
-
`**Description:** ${
|
|
239
|
-
`**Parameters:**\n${
|
|
281
|
+
: `\n\n✅ **Marketing Cloud Next:** Supported since API v${functionMcnSince}.0` +
|
|
282
|
+
(functionMcnNotes ? `\n> **MCN Note:** ${functionMcnNotes}` : '');
|
|
283
|
+
const text = `## ${function_.name}\n\n` +
|
|
284
|
+
`**Category:** ${function_.category ?? 'Unknown'}\n\n` +
|
|
285
|
+
`**Description:** ${function_.description ?? ''}\n\n` +
|
|
286
|
+
`**Parameters:**\n${parameters || ' (none)'}` +
|
|
240
287
|
examples +
|
|
241
288
|
mcnLine;
|
|
242
289
|
return { content: [{ type: 'text', text }] };
|
|
@@ -289,6 +336,90 @@ server.tool('list_ampscript_functions', 'List all AMPscript functions, optionall
|
|
|
289
336
|
};
|
|
290
337
|
});
|
|
291
338
|
// ---------------------------------------------------------------------------
|
|
339
|
+
// Tool: validate_handlebars
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
server.tool('validate_handlebars', 'Validate Marketing Cloud Next (MCN) Handlebars template code. ' +
|
|
342
|
+
'Checks helper names, arity, block balance, and flags unsupported constructs ' +
|
|
343
|
+
'(partials, decorators, and built-in helpers absent from the locked-down MCN engine). ' +
|
|
344
|
+
'MCN Handlebars lives inside the combined sfmc language and is always validated for the ' +
|
|
345
|
+
"'next' target.", {
|
|
346
|
+
code: z
|
|
347
|
+
.string()
|
|
348
|
+
.describe('The MCN Handlebars template code to validate. May include HTML context.'),
|
|
349
|
+
maxProblems: z
|
|
350
|
+
.number()
|
|
351
|
+
.int()
|
|
352
|
+
.min(1)
|
|
353
|
+
.max(500)
|
|
354
|
+
.optional()
|
|
355
|
+
.describe('Maximum number of problems to return (default 100).'),
|
|
356
|
+
}, ({ code, maxProblems }) => {
|
|
357
|
+
const settings = {
|
|
358
|
+
maxNumberOfProblems: maxProblems ?? 100,
|
|
359
|
+
targetPlatform: 'next',
|
|
360
|
+
};
|
|
361
|
+
const diagnostics = validateAmpscript(code, settings).filter((d) => d.source === 'handlebars');
|
|
362
|
+
return {
|
|
363
|
+
content: [{ type: 'text', text: formatDiagnostics(diagnostics) }],
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// Tool: lookup_handlebars_helper
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
server.tool('lookup_handlebars_helper', 'Look up the signature, parameters, description, and origin for a Marketing Cloud Next ' +
|
|
370
|
+
'Handlebars helper by name. Case-insensitive. Returns null if the helper is not found.', {
|
|
371
|
+
name: z
|
|
372
|
+
.string()
|
|
373
|
+
.describe('The Handlebars helper name, e.g. "uppercase", "if", "formatDate".'),
|
|
374
|
+
}, ({ name }) => {
|
|
375
|
+
const helper = sfmcLanguageService.lookupHandlebarsHelper(name);
|
|
376
|
+
if (!helper) {
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: 'text', text: `Handlebars helper "${name}" not found.` }],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return { content: [{ type: 'text', text: formatHandlebarsHelper(helper) }] };
|
|
382
|
+
});
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Tool: list_handlebars_helpers
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
server.tool('list_handlebars_helpers', 'List all Marketing Cloud Next Handlebars helpers, optionally filtered by category ' +
|
|
387
|
+
'and/or origin (handlebars-builtin, mcn-helper, mcn-platform).', {
|
|
388
|
+
category: z
|
|
389
|
+
.string()
|
|
390
|
+
.optional()
|
|
391
|
+
.describe('Filter by helper category (case-insensitive substring match), e.g. "string", "date", "comparison".'),
|
|
392
|
+
origin: z
|
|
393
|
+
.enum(['handlebars-builtin', 'mcn-helper', 'mcn-platform'])
|
|
394
|
+
.optional()
|
|
395
|
+
.describe('Filter by helper origin.'),
|
|
396
|
+
}, ({ category, origin }) => {
|
|
397
|
+
let helpers = sfmcLanguageService.listHandlebarsHelpers();
|
|
398
|
+
if (origin) {
|
|
399
|
+
helpers = helpers.filter((h) => h.origin === origin);
|
|
400
|
+
}
|
|
401
|
+
if (category) {
|
|
402
|
+
const catLower = category.toLowerCase();
|
|
403
|
+
helpers = helpers.filter((h) => h.category.toLowerCase().includes(catLower));
|
|
404
|
+
}
|
|
405
|
+
if (helpers.length === 0) {
|
|
406
|
+
return {
|
|
407
|
+
content: [{ type: 'text', text: 'No Handlebars helpers match the filter.' }],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const rows = helpers
|
|
411
|
+
.map((h) => `- **${h.name}** *(${h.category}, ${h.origin}, ${h.helperType})*: ${h.description}`)
|
|
412
|
+
.join('\n');
|
|
413
|
+
return {
|
|
414
|
+
content: [
|
|
415
|
+
{
|
|
416
|
+
type: 'text',
|
|
417
|
+
text: `## MCN Handlebars Helpers\n\n${rows}`,
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
292
423
|
// ECMAScript-builtin support lookup, sourced exclusively from ssjs-data.
|
|
293
424
|
// Used as a fall-through inside lookup_ssjs_function so a single tool answers
|
|
294
425
|
// "can I use X in SSJS?" for both SFMC APIs and plain-JavaScript built-ins.
|
|
@@ -422,35 +553,37 @@ server.tool('lookup_ssjs_function', 'Authoritative lookup (sourced ONLY from the
|
|
|
422
553
|
}, ({ name, owner }) => {
|
|
423
554
|
// Strip namespace prefix for lookup
|
|
424
555
|
const bare = name.replace(/^(Platform\.(Function|Variable|Response|Request|ClientBrowser|Recipient|DateTime)\.|WSProxy\.|HTTP\.|Script\.Util\.|Function\.|Variable\.|Response\.|Request\.)/i, '');
|
|
425
|
-
const
|
|
426
|
-
if (!
|
|
556
|
+
const function_ = sfmcLanguageService.lookupSsjsFunction(bare);
|
|
557
|
+
if (!function_) {
|
|
427
558
|
// Fall through to the ECMAScript built-in / polyfill / unsupported catalogs.
|
|
428
559
|
const builtinResult = lookupSsjsBuiltin(name, owner);
|
|
429
560
|
return { content: [{ type: 'text', text: builtinResult.text }] };
|
|
430
561
|
}
|
|
431
|
-
const
|
|
562
|
+
const parameters = (function_.params ?? [])
|
|
432
563
|
.map((p) => {
|
|
433
564
|
const isOptional = p.optional || p.required === false;
|
|
434
|
-
const
|
|
435
|
-
return ` - ${p.name}: ${p.type ?? 'any'} ${
|
|
565
|
+
const request = isOptional ? '(optional)' : '(required)';
|
|
566
|
+
return ` - ${p.name}: ${p.type ?? 'any'} ${request}${p.description ? ' — ' + p.description : ''}`;
|
|
436
567
|
})
|
|
437
568
|
.join('\n');
|
|
438
569
|
const badges = [];
|
|
439
|
-
if (
|
|
570
|
+
if (function_.deprecated) {
|
|
440
571
|
badges.push('⚠️ **Deprecated** — avoid in new code.');
|
|
441
572
|
}
|
|
442
|
-
if (
|
|
573
|
+
if (function_.requiresCoreLoad) {
|
|
443
574
|
badges.push('⚠️ **Requires** `Platform.Load("core", "1.1.5")` before calling this method.');
|
|
444
575
|
}
|
|
445
|
-
const header =
|
|
576
|
+
const header = function_.isStatic
|
|
577
|
+
? `## ${function_.name} *(static)*`
|
|
578
|
+
: `## ${function_.name}`;
|
|
446
579
|
const badgeBlock = badges.length > 0 ? badges.join('\n') + '\n\n' : '';
|
|
447
|
-
const aliasLine =
|
|
580
|
+
const aliasLine = function_.aliasOf ? `**Alias of:** \`${function_.aliasOf}\`\n\n` : '';
|
|
448
581
|
const text = `${header}\n\n` +
|
|
449
582
|
badgeBlock +
|
|
450
583
|
aliasLine +
|
|
451
|
-
`**Description:** ${
|
|
452
|
-
`**Parameters:**\n${
|
|
453
|
-
`**Returns:** ${
|
|
584
|
+
`**Description:** ${function_.description ?? ''}\n\n` +
|
|
585
|
+
`**Parameters:**\n${parameters || ' (none)'}\n\n` +
|
|
586
|
+
`**Returns:** ${function_.returnType ?? 'void'}`;
|
|
454
587
|
return { content: [{ type: 'text', text }] };
|
|
455
588
|
});
|
|
456
589
|
// ---------------------------------------------------------------------------
|
|
@@ -474,13 +607,13 @@ server.tool('review_change', 'Review a code diff for SFMC (AMPscript, SSJS, or H
|
|
|
474
607
|
}, ({ diff, language = 'auto', maxProblems = 50 }) => {
|
|
475
608
|
// Extract added lines from the unified diff
|
|
476
609
|
const addedLines = [];
|
|
477
|
-
let
|
|
610
|
+
let lineNumber = 0;
|
|
478
611
|
const lineMap = []; // maps index in addedLines to original diff line number
|
|
479
612
|
for (const line of diff.split('\n')) {
|
|
480
|
-
|
|
613
|
+
lineNumber++;
|
|
481
614
|
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
482
615
|
addedLines.push(line.slice(1));
|
|
483
|
-
lineMap.push(
|
|
616
|
+
lineMap.push(lineNumber);
|
|
484
617
|
}
|
|
485
618
|
}
|
|
486
619
|
if (addedLines.length === 0) {
|
|
@@ -493,8 +626,8 @@ server.tool('review_change', 'Review a code diff for SFMC (AMPscript, SSJS, or H
|
|
|
493
626
|
? detectLanguage(addedCode, 'html')
|
|
494
627
|
: language;
|
|
495
628
|
const settings = { maxNumberOfProblems: maxProblems };
|
|
496
|
-
const
|
|
497
|
-
const diagnostics = sfmcLanguageService.validate(
|
|
629
|
+
const document = { text: addedCode, languageId: detectedLang, uri: 'diff' };
|
|
630
|
+
const diagnostics = sfmcLanguageService.validate(document, settings);
|
|
498
631
|
if (diagnostics.length === 0) {
|
|
499
632
|
return {
|
|
500
633
|
content: [
|
|
@@ -509,7 +642,7 @@ server.tool('review_change', 'Review a code diff for SFMC (AMPscript, SSJS, or H
|
|
|
509
642
|
for (const d of diagnostics) {
|
|
510
643
|
const sev = d.severity === 1 ? '🔴 ERROR' : d.severity === 2 ? '🟡 WARNING' : '🔵 INFO';
|
|
511
644
|
const origLine = lineMap[d.range.start.line] ?? d.range.start.line + 1;
|
|
512
|
-
const message =
|
|
645
|
+
const message = diagnosticMessage(d.message);
|
|
513
646
|
output.push(`${sev} (diff line ${origLine}): ${message}`);
|
|
514
647
|
}
|
|
515
648
|
return { content: [{ type: 'text', text: output.join('\n') }] };
|
|
@@ -538,15 +671,15 @@ server.tool('suggest_fix', 'Generate a corrected version of SFMC code based on v
|
|
|
538
671
|
? detectLanguage(code)
|
|
539
672
|
: detectLanguage(code, language);
|
|
540
673
|
const settings = { maxNumberOfProblems: 50, targetPlatform: target };
|
|
541
|
-
const
|
|
542
|
-
const diagnostics = sfmcLanguageService.validate(
|
|
674
|
+
const document = { text: code, languageId: detectedLang, uri: 'fix-target' };
|
|
675
|
+
const diagnostics = sfmcLanguageService.validate(document, settings);
|
|
543
676
|
const lines = code.split('\n');
|
|
544
677
|
const suggestions = [];
|
|
545
678
|
for (const d of diagnostics) {
|
|
546
679
|
const lineText = lines[d.range.start.line] ?? '';
|
|
547
680
|
// Diagnostic.message widened to `string | MarkupContent` in newer LSP types; the
|
|
548
681
|
// SFMC language service only emits plain strings, so unwrap MarkupContent to its value.
|
|
549
|
-
const message =
|
|
682
|
+
const message = diagnosticMessage(d.message);
|
|
550
683
|
suggestions.push(`Line ${d.range.start.line + 1}: ${message}\n` +
|
|
551
684
|
` Code: ${lineText.trim()}\n` +
|
|
552
685
|
` Fix: ${getFixSuggestion(message, lineText, detectedLang)}`);
|
|
@@ -568,6 +701,24 @@ server.tool('suggest_fix', 'Generate a corrected version of SFMC code based on v
|
|
|
568
701
|
*/
|
|
569
702
|
function getFixSuggestion(message, line, lang) {
|
|
570
703
|
const m = message.toLowerCase();
|
|
704
|
+
// MCN Handlebars diagnostics (surfaced when target === 'next' on {{…}} regions).
|
|
705
|
+
if (m.includes('partials ({{>'))
|
|
706
|
+
return 'Inline the partial content directly — the locked-down MCN engine cannot register partials.';
|
|
707
|
+
if (m.includes('partial blocks'))
|
|
708
|
+
return 'Inline the block content directly — the MCN engine cannot register partial blocks.';
|
|
709
|
+
if (m.includes('decorators ({{*') || m.includes('decorator blocks'))
|
|
710
|
+
return 'Remove the decorator — the MCN engine cannot register decorators.';
|
|
711
|
+
if (m.includes('{{log}}'))
|
|
712
|
+
return 'Remove the {{log}} debugging helper — it is not available in MCN Handlebars.';
|
|
713
|
+
if (m.includes('unknown handlebars helper')) {
|
|
714
|
+
const helperMatch = message.match(/'([^']+)'/);
|
|
715
|
+
const hint = helperMatch
|
|
716
|
+
? ` Check spelling or use a catalog helper instead of "${helperMatch[1]}".`
|
|
717
|
+
: '';
|
|
718
|
+
return `Use a helper from the MCN Handlebars catalog (list_handlebars_helpers); custom helpers cannot be registered.${hint}`;
|
|
719
|
+
}
|
|
720
|
+
if (m.includes('handlebars for marketing cloud next') || m.includes('mcn engine'))
|
|
721
|
+
return 'Replace with a supported MCN Handlebars construct — see list_handlebars_helpers.';
|
|
571
722
|
if (m.includes("'let'") || m.includes("'const'"))
|
|
572
723
|
return 'Replace `let`/`const` with `var`.';
|
|
573
724
|
if (m.includes('arrow function'))
|
|
@@ -583,9 +734,9 @@ function getFixSuggestion(message, line, lang) {
|
|
|
583
734
|
if (m.includes('html comment'))
|
|
584
735
|
return 'Remove the `<!-- -->` wrapper; use `/* comment */` inside AMPscript.';
|
|
585
736
|
if (m.includes('unknown function')) {
|
|
586
|
-
const
|
|
587
|
-
if (
|
|
588
|
-
return `Check spelling — did you mean a known AMPscript function? ("${
|
|
737
|
+
const functionMatch = message.match(/"([^"]+)"/);
|
|
738
|
+
if (functionMatch)
|
|
739
|
+
return `Check spelling — did you mean a known AMPscript function? ("${functionMatch[1]}")`;
|
|
589
740
|
}
|
|
590
741
|
if (m.includes('expects'))
|
|
591
742
|
return 'Check the number and types of arguments against the function signature.';
|
|
@@ -606,13 +757,11 @@ server.tool('get_ampscript_completions', 'Return a list of AMPscript function na
|
|
|
606
757
|
.optional()
|
|
607
758
|
.describe("Target platform. Use 'next' to return only completions supported in Marketing Cloud Next."),
|
|
608
759
|
}, ({ code, line, character, target }) => {
|
|
609
|
-
const
|
|
610
|
-
let items = sfmcLanguageService.getCompletions(
|
|
760
|
+
const document = { text: code, languageId: 'ampscript', uri: 'completions' };
|
|
761
|
+
let items = sfmcLanguageService.getCompletions(document, { line, character });
|
|
611
762
|
if (target === 'next') {
|
|
612
763
|
items = items.filter((item) => {
|
|
613
|
-
const label =
|
|
614
|
-
? item.label
|
|
615
|
-
: item.label.label;
|
|
764
|
+
const label = labelText(item);
|
|
616
765
|
// Keep keywords and variables (non-function entries); filter out MCN-unsupported functions
|
|
617
766
|
return !label.includes('(') || isMcnSupported(label.replace(/\(.*/, '').trim());
|
|
618
767
|
});
|
|
@@ -620,9 +769,7 @@ server.tool('get_ampscript_completions', 'Return a list of AMPscript function na
|
|
|
620
769
|
const formatted = items
|
|
621
770
|
.slice(0, 50)
|
|
622
771
|
.map((item) => {
|
|
623
|
-
const label =
|
|
624
|
-
? item.label
|
|
625
|
-
: item.label.label;
|
|
772
|
+
const label = labelText(item);
|
|
626
773
|
return `- ${label}${item.detail ? ` — ${item.detail}` : ''}`;
|
|
627
774
|
})
|
|
628
775
|
.join('\n');
|
|
@@ -665,19 +812,12 @@ server.tool('get_ssjs_completions', 'Return a list of SSJS Platform functions, W
|
|
|
665
812
|
}
|
|
666
813
|
const items = sfmcLanguageService.getSsjsCompletionCatalog();
|
|
667
814
|
const filtered = filter
|
|
668
|
-
? items.filter((item) =>
|
|
669
|
-
const label = typeof item.label === 'string'
|
|
670
|
-
? item.label
|
|
671
|
-
: item.label.label;
|
|
672
|
-
return label.toLowerCase().startsWith(filter.toLowerCase());
|
|
673
|
-
})
|
|
815
|
+
? items.filter((item) => labelText(item).toLowerCase().startsWith(filter.toLowerCase()))
|
|
674
816
|
: items;
|
|
675
817
|
const formatted = filtered
|
|
676
818
|
.slice(0, 80)
|
|
677
819
|
.map((item) => {
|
|
678
|
-
const label =
|
|
679
|
-
? item.label
|
|
680
|
-
: item.label.label;
|
|
820
|
+
const label = labelText(item);
|
|
681
821
|
return `- ${label}${item.detail ? ` — ${item.detail}` : ''}`;
|
|
682
822
|
})
|
|
683
823
|
.join('\n');
|
|
@@ -693,12 +833,74 @@ server.tool('get_ssjs_completions', 'Return a list of SSJS Platform functions, W
|
|
|
693
833
|
};
|
|
694
834
|
});
|
|
695
835
|
// ---------------------------------------------------------------------------
|
|
836
|
+
// Tool: get_handlebars_completions
|
|
837
|
+
// ---------------------------------------------------------------------------
|
|
838
|
+
server.tool('get_handlebars_completions', 'Return Marketing Cloud Next (MCN) Handlebars helper completions with snippet insert text. ' +
|
|
839
|
+
'MCN Handlebars is only available on the next platform, so this catalog is always the ' +
|
|
840
|
+
'\'next\' set. Optionally filter by a name prefix (e.g. "form", "date").', {
|
|
841
|
+
filter: z
|
|
842
|
+
.string()
|
|
843
|
+
.optional()
|
|
844
|
+
.describe('Optional helper-name prefix filter (case-insensitive), e.g. "form", "to".'),
|
|
845
|
+
}, ({ filter }) => {
|
|
846
|
+
const items = sfmcLanguageService.getHandlebarsCompletionCatalog();
|
|
847
|
+
const filtered = filter
|
|
848
|
+
? items.filter((item) => labelText(item).toLowerCase().startsWith(filter.toLowerCase()))
|
|
849
|
+
: items;
|
|
850
|
+
const formatted = filtered
|
|
851
|
+
.slice(0, 80)
|
|
852
|
+
.map((item) => {
|
|
853
|
+
const label = labelText(item);
|
|
854
|
+
return `- ${label}${item.detail ? ` — ${item.detail}` : ''}`;
|
|
855
|
+
})
|
|
856
|
+
.join('\n');
|
|
857
|
+
return {
|
|
858
|
+
content: [
|
|
859
|
+
{
|
|
860
|
+
type: 'text',
|
|
861
|
+
text: filtered.length === 0
|
|
862
|
+
? `No MCN Handlebars helpers matching "${filter}".`
|
|
863
|
+
: `${filtered.length} MCN Handlebars helper completions${filter ? ` matching "${filter}"` : ''} (showing up to 80):\n\n${formatted}`,
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
};
|
|
867
|
+
});
|
|
868
|
+
// ---------------------------------------------------------------------------
|
|
696
869
|
// Tool: format_sfmc_code (basic, no prettier integration needed)
|
|
697
870
|
// ---------------------------------------------------------------------------
|
|
698
|
-
|
|
699
|
-
|
|
871
|
+
/**
|
|
872
|
+
* Conservatively normalize whitespace inside Marketing Cloud Next (MCN)
|
|
873
|
+
* Handlebars `{{…}}` mustaches: collapse runs of internal whitespace to a
|
|
874
|
+
* single space and trim the edges (e.g. `{{ foo bar }}` → `{{foo bar}}`),
|
|
875
|
+
* matching what Prettier's Glimmer parser does for mustache interiors.
|
|
876
|
+
*
|
|
877
|
+
* Hard guardrails (never altered):
|
|
878
|
+
* - `{!$…}` merge-field bindings — they are not Handlebars syntax.
|
|
879
|
+
* - `{{!-- … --}}` / `{{! … }}` comments — preserved byte-for-byte.
|
|
880
|
+
*
|
|
881
|
+
* This is the deferred-Prettier stub for item 8 of the MCN Handlebars handoff:
|
|
882
|
+
* full routing through prettier-plugin-sfmc's Glimmer path lands once that
|
|
883
|
+
* plugin ships Handlebars support (see HANDOFF-prettier-handlebars.md).
|
|
884
|
+
* @param {string} code - The Handlebars-in-HTML code to normalize.
|
|
885
|
+
* @returns {string} The code with mustache interiors normalized.
|
|
886
|
+
*/
|
|
887
|
+
function normalizeHandlebarsWhitespace(code) {
|
|
888
|
+
return code.replaceAll(/\{\{([\s\S]*?)\}\}/g, (full, inner) => {
|
|
889
|
+
// Preserve comments verbatim.
|
|
890
|
+
if (inner.startsWith('!')) {
|
|
891
|
+
return full;
|
|
892
|
+
}
|
|
893
|
+
const normalized = inner.replaceAll(/\s+/g, ' ').trim();
|
|
894
|
+
return `{{${normalized}}}`;
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
server.tool('format_sfmc_code', 'Apply basic formatting conventions to AMPscript, SSJS, or Marketing Cloud Next (MCN) ' +
|
|
898
|
+
'Handlebars code. Normalises keyword casing and whitespace. For Handlebars, collapses ' +
|
|
899
|
+
'whitespace inside {{…}} mustaches while leaving {!$…} bindings and {{!-- … --}} comments ' +
|
|
900
|
+
'untouched. (Handlebars formatting is a conservative whitespace normalizer; full Prettier ' +
|
|
901
|
+
'Glimmer routing is deferred until prettier-plugin-sfmc ships Handlebars support.)', {
|
|
700
902
|
code: z.string().describe('The SFMC code to format.'),
|
|
701
|
-
language: z.enum(['ampscript', 'ssjs']).describe('The language of the code.'),
|
|
903
|
+
language: z.enum(['ampscript', 'ssjs', 'handlebars']).describe('The language of the code.'),
|
|
702
904
|
}, ({ code, language }) => {
|
|
703
905
|
let formatted = code;
|
|
704
906
|
if (language === 'ampscript') {
|
|
@@ -709,6 +911,9 @@ server.tool('format_sfmc_code', 'Apply basic formatting conventions to AMPscript
|
|
|
709
911
|
.replaceAll(/\bOR\b/gi, 'OR')
|
|
710
912
|
.replaceAll(/\bNOT\b/gi, 'NOT');
|
|
711
913
|
}
|
|
914
|
+
else if (language === 'handlebars') {
|
|
915
|
+
formatted = normalizeHandlebarsWhitespace(formatted);
|
|
916
|
+
}
|
|
712
917
|
else {
|
|
713
918
|
// SSJS: normalise Platform.Load to use double quotes
|
|
714
919
|
formatted = formatted.replaceAll(/Platform\.Load\s*\(\s*'([^']*)'\s*,\s*'([^']*)'\s*\)/g, 'Platform.Load("$1", "$2")');
|
|
@@ -759,9 +964,9 @@ server.tool('search_mce_help', MCE_HELP_TOOL_DESC, {
|
|
|
759
964
|
: `No matches for this query with product_focus="${focus}". Try broader keywords or product_focus="any".`;
|
|
760
965
|
return { content: [{ type: 'text', text: hint }] };
|
|
761
966
|
}
|
|
762
|
-
const lines = hits.map((h,
|
|
967
|
+
const lines = hits.map((h, index) => {
|
|
763
968
|
const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
|
|
764
|
-
return (`### ${
|
|
969
|
+
return (`### ${index + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
|
|
765
970
|
`**Product:** ${h.chunk.productLabel}\n` +
|
|
766
971
|
`**Score:** ${h.score}\n\n` +
|
|
767
972
|
`${excerpt}${h.chunk.body.length > 520 ? '…' : ''}\n`);
|
|
@@ -775,11 +980,11 @@ server.tool('search_mce_help', MCE_HELP_TOOL_DESC, {
|
|
|
775
980
|
// ---------------------------------------------------------------------------
|
|
776
981
|
server.resource('ampscript-function-catalog', 'sfmc://ampscript/functions', async () => {
|
|
777
982
|
const functions = sfmcLanguageService.getAllAmpscriptFunctions();
|
|
778
|
-
const lines = functions.map((
|
|
779
|
-
const
|
|
983
|
+
const lines = functions.map((function_) => {
|
|
984
|
+
const parameterList = function_.params
|
|
780
985
|
.map((p) => p.optional ? `[${p.name}: ${p.type ?? 'any'}]` : `${p.name}: ${p.type ?? 'any'}`)
|
|
781
986
|
.join(', ');
|
|
782
|
-
return `${
|
|
987
|
+
return `${function_.name}(${parameterList}) — ${function_.description ?? ''}`;
|
|
783
988
|
});
|
|
784
989
|
return {
|
|
785
990
|
contents: [
|
|
@@ -797,13 +1002,13 @@ server.resource('ampscript-function-catalog', 'sfmc://ampscript/functions', asyn
|
|
|
797
1002
|
// ---------------------------------------------------------------------------
|
|
798
1003
|
server.resource('ssjs-function-catalog', 'sfmc://ssjs/functions', async () => {
|
|
799
1004
|
const functions = sfmcLanguageService.getAllSsjsFunctions();
|
|
800
|
-
const lines = functions.map((
|
|
801
|
-
const
|
|
1005
|
+
const lines = functions.map((function_) => {
|
|
1006
|
+
const parameterList = (function_.params ?? [])
|
|
802
1007
|
.map((p) => p.optional || p.required === false
|
|
803
1008
|
? `[${p.name}: ${p.type ?? 'any'}]`
|
|
804
1009
|
: `${p.name}: ${p.type ?? 'any'}`)
|
|
805
1010
|
.join(', ');
|
|
806
|
-
return `${
|
|
1011
|
+
return `${function_.name}(${parameterList}) — ${function_.description ?? ''}`;
|
|
807
1012
|
});
|
|
808
1013
|
return {
|
|
809
1014
|
contents: [
|
|
@@ -817,6 +1022,48 @@ server.resource('ssjs-function-catalog', 'sfmc://ssjs/functions', async () => {
|
|
|
817
1022
|
};
|
|
818
1023
|
});
|
|
819
1024
|
// ---------------------------------------------------------------------------
|
|
1025
|
+
// Resource: handlebars-helper-catalog
|
|
1026
|
+
// ---------------------------------------------------------------------------
|
|
1027
|
+
server.resource('handlebars-helper-catalog', 'sfmc://handlebars/helpers', async () => {
|
|
1028
|
+
const helpers = sfmcLanguageService.listHandlebarsHelpers();
|
|
1029
|
+
const lines = helpers.map((h) => {
|
|
1030
|
+
const parameterList = h.params
|
|
1031
|
+
.map((p) => {
|
|
1032
|
+
const inner = `${p.name}: ${p.type}`;
|
|
1033
|
+
return p.optional ? `[${inner}]` : inner;
|
|
1034
|
+
})
|
|
1035
|
+
.join(', ');
|
|
1036
|
+
return `{{${h.name} ${parameterList}}} — (${h.category}, ${h.origin}, v${h.mcnSince}.0+) ${h.description}`;
|
|
1037
|
+
});
|
|
1038
|
+
return {
|
|
1039
|
+
contents: [
|
|
1040
|
+
{
|
|
1041
|
+
uri: 'sfmc://handlebars/helpers',
|
|
1042
|
+
mimeType: 'text/plain',
|
|
1043
|
+
text: `# MCN Handlebars Helper Catalog (${helpers.length} helpers)\n\n` +
|
|
1044
|
+
lines.join('\n'),
|
|
1045
|
+
},
|
|
1046
|
+
],
|
|
1047
|
+
};
|
|
1048
|
+
});
|
|
1049
|
+
// ---------------------------------------------------------------------------
|
|
1050
|
+
// Resource: handlebars-binding-catalog
|
|
1051
|
+
// ---------------------------------------------------------------------------
|
|
1052
|
+
server.resource('handlebars-binding-catalog', 'sfmc://handlebars/bindings', async () => {
|
|
1053
|
+
const bindings = sfmcLanguageService.listHandlebarsBindings();
|
|
1054
|
+
const lines = bindings.map((b) => `${b.token} — (${b.namespace}, v${b.mcnSince}.0+) ${b.description}`);
|
|
1055
|
+
return {
|
|
1056
|
+
contents: [
|
|
1057
|
+
{
|
|
1058
|
+
uri: 'sfmc://handlebars/bindings',
|
|
1059
|
+
mimeType: 'text/plain',
|
|
1060
|
+
text: `# MCN Handlebars Built-in Binding Catalog (${bindings.length} bindings)\n\n` +
|
|
1061
|
+
lines.join('\n'),
|
|
1062
|
+
},
|
|
1063
|
+
],
|
|
1064
|
+
};
|
|
1065
|
+
});
|
|
1066
|
+
// ---------------------------------------------------------------------------
|
|
820
1067
|
// Resource: ampscript-keywords
|
|
821
1068
|
// ---------------------------------------------------------------------------
|
|
822
1069
|
server.resource('ampscript-keywords', 'sfmc://ampscript/keywords', async () => {
|
|
@@ -902,7 +1149,7 @@ server.resource('mce-product-context', 'sfmc://mce/product-context', async () =>
|
|
|
902
1149
|
// ---------------------------------------------------------------------------
|
|
903
1150
|
server.resource('mce-help-index', 'sfmc://mce/help-index', async () => {
|
|
904
1151
|
const chunks = getChunks();
|
|
905
|
-
const files = [...new Set(chunks.map((c) => c.relativePath))].sort();
|
|
1152
|
+
const files = [...new Set(chunks.map((c) => c.relativePath))].sort((a, b) => a.localeCompare(b));
|
|
906
1153
|
const stats = getMceHelpStats();
|
|
907
1154
|
const scopeRows = Object.entries(stats.breakdown)
|
|
908
1155
|
.sort(([, a], [, b]) => b - a)
|
|
@@ -921,7 +1168,7 @@ server.resource('mce-help-index', 'sfmc://mce/help-index', async () => {
|
|
|
921
1168
|
// ---------------------------------------------------------------------------
|
|
922
1169
|
server.resource('mcn-help-index', 'sfmc://mcn/help-index', async () => {
|
|
923
1170
|
const chunks = getMcnChunks();
|
|
924
|
-
const files = [...new Set(chunks.map((c) => c.relativePath))].sort();
|
|
1171
|
+
const files = [...new Set(chunks.map((c) => c.relativePath))].sort((a, b) => a.localeCompare(b));
|
|
925
1172
|
const stats = getMcnHelpStats();
|
|
926
1173
|
const text = `# Bundled Marketing Cloud Next developer API docs (${stats.chunkCount} sections from ${stats.fileCount} files)\n\n` +
|
|
927
1174
|
`## Files\n\n` +
|
|
@@ -1032,16 +1279,16 @@ server.tool('search_help', 'Unified help search that automatically detects the t
|
|
|
1032
1279
|
const sections = [];
|
|
1033
1280
|
if (platform === 'next') {
|
|
1034
1281
|
// MCN: search both the developer API reference and the MCN operational/admin help
|
|
1035
|
-
const
|
|
1282
|
+
const developmentHits = searchMcnHelp(query, Math.ceil(limit / 2));
|
|
1036
1283
|
const opsHits = searchMceHelp(query, Math.floor(limit / 2), 'next');
|
|
1037
|
-
if (
|
|
1038
|
-
const parts =
|
|
1284
|
+
if (developmentHits.length > 0) {
|
|
1285
|
+
const parts = developmentHits.map(({ chunk }) => `### ${chunk.heading}\n*Source: ${chunk.relativePath}*\n\n${chunk.body}`);
|
|
1039
1286
|
sections.push(`## MCN Developer API Docs\n\n${parts.join('\n\n---\n\n')}`);
|
|
1040
1287
|
}
|
|
1041
1288
|
if (opsHits.length > 0) {
|
|
1042
|
-
const parts = opsHits.map((h,
|
|
1289
|
+
const parts = opsHits.map((h, index) => {
|
|
1043
1290
|
const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
|
|
1044
|
-
return (`### ${
|
|
1291
|
+
return (`### ${index + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
|
|
1045
1292
|
`**Score:** ${h.score}\n\n` +
|
|
1046
1293
|
`${excerpt}${h.chunk.body.length > 520 ? '…' : ''}`);
|
|
1047
1294
|
});
|
|
@@ -1071,9 +1318,9 @@ server.tool('search_help', 'Unified help search that automatically detects the t
|
|
|
1071
1318
|
: `No results found for "${query}". Try broader keywords.`;
|
|
1072
1319
|
return { content: [{ type: 'text', text: hint }] };
|
|
1073
1320
|
}
|
|
1074
|
-
const parts = hits.map((h,
|
|
1321
|
+
const parts = hits.map((h, index) => {
|
|
1075
1322
|
const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
|
|
1076
|
-
return (`### ${
|
|
1323
|
+
return (`### ${index + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
|
|
1077
1324
|
`**Product:** ${h.chunk.productLabel}\n` +
|
|
1078
1325
|
`**Score:** ${h.score}\n\n` +
|
|
1079
1326
|
`${excerpt}${h.chunk.body.length > 520 ? '…' : ''}`);
|
|
@@ -1244,6 +1491,23 @@ server.prompt('reviewSfmcCode', 'Review SFMC code for correctness, best practice
|
|
|
1244
1491
|
const platformNote = target === 'next'
|
|
1245
1492
|
? '\n- **Marketing Cloud Next compatibility**: Flag any AMPscript functions not supported in MCN (API v67.0+), and any SSJS blocks (SSJS is not supported in MCN).'
|
|
1246
1493
|
: '';
|
|
1494
|
+
const fenceLang = detectedLang === 'ssjs' ? 'javascript' : detectedLang;
|
|
1495
|
+
const checklistItems = detectedLang === 'ampscript'
|
|
1496
|
+
? [
|
|
1497
|
+
'- Delimiter balance (%%[ ]%%, %%= =%%)',
|
|
1498
|
+
'- IF/ENDIF, FOR/NEXT block balance',
|
|
1499
|
+
'- Correct function names and argument counts',
|
|
1500
|
+
'- Correct comment syntax (/* */ only)',
|
|
1501
|
+
'- Proper variable declaration with @',
|
|
1502
|
+
]
|
|
1503
|
+
: [
|
|
1504
|
+
'- No ES6+ syntax (var, not let/const; no arrow functions)',
|
|
1505
|
+
'- Platform.Load before Core library objects',
|
|
1506
|
+
'- Correct Platform.Function calls',
|
|
1507
|
+
'- WSProxy error handling',
|
|
1508
|
+
'- No sensitive data in logs or responses',
|
|
1509
|
+
];
|
|
1510
|
+
const checklist = checklistItems.join('\n') + platformNote;
|
|
1247
1511
|
return {
|
|
1248
1512
|
messages: [
|
|
1249
1513
|
{
|
|
@@ -1260,26 +1524,12 @@ server.prompt('reviewSfmcCode', 'Review SFMC code for correctness, best practice
|
|
|
1260
1524
|
focus ? `Focus especially on: ${focus}` : '',
|
|
1261
1525
|
'',
|
|
1262
1526
|
'## Code to Review',
|
|
1263
|
-
'```' +
|
|
1527
|
+
'```' + fenceLang,
|
|
1264
1528
|
code,
|
|
1265
1529
|
'```',
|
|
1266
1530
|
'',
|
|
1267
1531
|
'## Review checklist',
|
|
1268
|
-
|
|
1269
|
-
? [
|
|
1270
|
-
'- Delimiter balance (%%[ ]%%, %%= =%%)',
|
|
1271
|
-
'- IF/ENDIF, FOR/NEXT block balance',
|
|
1272
|
-
'- Correct function names and argument counts',
|
|
1273
|
-
'- Correct comment syntax (/* */ only)',
|
|
1274
|
-
'- Proper variable declaration with @',
|
|
1275
|
-
].join('\n') + platformNote
|
|
1276
|
-
: [
|
|
1277
|
-
'- No ES6+ syntax (var, not let/const; no arrow functions)',
|
|
1278
|
-
'- Platform.Load before Core library objects',
|
|
1279
|
-
'- Correct Platform.Function calls',
|
|
1280
|
-
'- WSProxy error handling',
|
|
1281
|
-
'- No sensitive data in logs or responses',
|
|
1282
|
-
].join('\n') + platformNote,
|
|
1532
|
+
checklist,
|
|
1283
1533
|
]
|
|
1284
1534
|
.filter(Boolean)
|
|
1285
1535
|
.join('\n'),
|
|
@@ -1395,16 +1645,56 @@ server.prompt('answerMceHowTo', 'Answer a Marketing Cloud **administration or se
|
|
|
1395
1645
|
// ---------------------------------------------------------------------------
|
|
1396
1646
|
// Tool: check_mcn_compatibility
|
|
1397
1647
|
// ---------------------------------------------------------------------------
|
|
1648
|
+
const mcnFileObjectSchema = z.object({
|
|
1649
|
+
filename: z.string().describe('File name (e.g. "email-template.html").'),
|
|
1650
|
+
content: z.string().describe('Full file content to analyze.'),
|
|
1651
|
+
});
|
|
1652
|
+
const mcnFileArraySchema = z.array(mcnFileObjectSchema).describe('List of files to analyze.');
|
|
1653
|
+
/**
|
|
1654
|
+
* Returns the reason for the first non-migratable SSJS pattern that matches the
|
|
1655
|
+
* given block code, or an empty string when none match.
|
|
1656
|
+
* @param blockCode
|
|
1657
|
+
*/
|
|
1658
|
+
function firstNonMigratableSsjsReason(blockCode) {
|
|
1659
|
+
for (const { pattern, reason } of NON_MIGRATABLE_SSJS_PATTERNS) {
|
|
1660
|
+
pattern.lastIndex = 0;
|
|
1661
|
+
if (pattern.test(blockCode)) {
|
|
1662
|
+
return reason;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return '';
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Scans content for recognized Handlebars helper usages, deduplicated by
|
|
1669
|
+
* helper name and approximate line. Matching is data-driven via the LSP lookup.
|
|
1670
|
+
* @param content
|
|
1671
|
+
*/
|
|
1672
|
+
function collectHandlebarsHelperUsages(content) {
|
|
1673
|
+
const helperCallPattern = /\{\{[#/]?\s*([a-zA-Z][\w-]*)/g;
|
|
1674
|
+
const seenHelper = new Set();
|
|
1675
|
+
const usages = [];
|
|
1676
|
+
let helperMatch;
|
|
1677
|
+
while ((helperMatch = helperCallPattern.exec(content)) !== null) {
|
|
1678
|
+
const token = helperMatch[1];
|
|
1679
|
+
const helper = sfmcLanguageService.lookupHandlebarsHelper(token);
|
|
1680
|
+
if (!helper) {
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
const line = content.slice(0, helperMatch.index).split('\n').length;
|
|
1684
|
+
const dedupeKey = `${helper.name}@${line}`;
|
|
1685
|
+
if (seenHelper.has(dedupeKey)) {
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
seenHelper.add(dedupeKey);
|
|
1689
|
+
usages.push({ name: helper.name, line, mcnSince: helper.mcnSince });
|
|
1690
|
+
}
|
|
1691
|
+
return usages;
|
|
1692
|
+
}
|
|
1398
1693
|
server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files for Marketing Cloud Next (MCN) readiness. ' +
|
|
1399
1694
|
'Returns an executive summary and a per-file, per-function report with migration difficulty. ' +
|
|
1400
1695
|
'SSJS blocks that only use Platform.Function.* calls are classified as "Needs conversion" (not "Not migratable"). ' +
|
|
1401
1696
|
'Use this tool before using rewrite_for_mcn.', {
|
|
1402
|
-
files:
|
|
1403
|
-
.array(z.object({
|
|
1404
|
-
filename: z.string().describe('File name (e.g. "email-template.html").'),
|
|
1405
|
-
content: z.string().describe('Full file content to analyze.'),
|
|
1406
|
-
}))
|
|
1407
|
-
.describe('List of files to analyze.'),
|
|
1697
|
+
files: mcnFileArraySchema,
|
|
1408
1698
|
}, ({ files }) => {
|
|
1409
1699
|
const results = [];
|
|
1410
1700
|
for (const { filename, content } of files) {
|
|
@@ -1417,7 +1707,15 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
|
|
|
1417
1707
|
const notes = getMcnNotes(site.name);
|
|
1418
1708
|
let status;
|
|
1419
1709
|
let reason;
|
|
1420
|
-
|
|
1710
|
+
const isHbsGap = AMP_MCN_HANDLEBARS_GAP.has(site.name.toLowerCase());
|
|
1711
|
+
if (isHbsGap) {
|
|
1712
|
+
// Category C: documented as MCN-supported but no working Handlebars
|
|
1713
|
+
// helper exists yet — fails at runtime. Always needs-review, even when
|
|
1714
|
+
// mcnSince is set, because the data flags a runtime gap.
|
|
1715
|
+
status = 'needs-review';
|
|
1716
|
+
reason = `${site.name}() is ${HBS_GAP_NOTE}`;
|
|
1717
|
+
}
|
|
1718
|
+
else if (mcnSince !== null && notes === null) {
|
|
1421
1719
|
status = 'supported';
|
|
1422
1720
|
reason = '—';
|
|
1423
1721
|
}
|
|
@@ -1444,14 +1742,7 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
|
|
|
1444
1742
|
const blockCode = blockMatch[1];
|
|
1445
1743
|
const lineApprox = content.slice(0, blockMatch.index).split('\n').length;
|
|
1446
1744
|
// Check for non-migratable patterns
|
|
1447
|
-
|
|
1448
|
-
for (const { pattern, reason } of NON_MIGRATABLE_SSJS_PATTERNS) {
|
|
1449
|
-
pattern.lastIndex = 0;
|
|
1450
|
-
if (pattern.test(blockCode)) {
|
|
1451
|
-
notMigratableReason = reason;
|
|
1452
|
-
break;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1745
|
+
const notMigratableReason = firstNonMigratableSsjsReason(blockCode);
|
|
1455
1746
|
if (notMigratableReason) {
|
|
1456
1747
|
ssjsBlocks.push({
|
|
1457
1748
|
index: blockIndex,
|
|
@@ -1469,28 +1760,61 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
|
|
|
1469
1760
|
});
|
|
1470
1761
|
}
|
|
1471
1762
|
}
|
|
1763
|
+
// 2b. Analyze MCN Handlebars regions (only meaningful for the 'next' target).
|
|
1764
|
+
// Diagnostics flag unsupported constructs (partials, decorators, built-in
|
|
1765
|
+
// helpers absent from the locked-down engine), unknown helpers, and arity.
|
|
1766
|
+
const handlebarsProblems = [];
|
|
1767
|
+
const handlebarsHelpers = [];
|
|
1768
|
+
if (content.includes('{{')) {
|
|
1769
|
+
const hbsDiagnostics = validateAmpscript(content, {
|
|
1770
|
+
maxNumberOfProblems: 200,
|
|
1771
|
+
targetPlatform: 'next',
|
|
1772
|
+
}).filter((d) => d.source === 'handlebars');
|
|
1773
|
+
for (const d of hbsDiagnostics) {
|
|
1774
|
+
const severity = d.severity === 1 ? 'error' : d.severity === 2 ? 'warning' : 'info';
|
|
1775
|
+
const message = diagnosticMessage(d.message);
|
|
1776
|
+
handlebarsProblems.push({
|
|
1777
|
+
line: d.range.start.line + 1,
|
|
1778
|
+
severity,
|
|
1779
|
+
message,
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
// Report recognized helper usages with their MCN availability version.
|
|
1783
|
+
// Helper names are matched against handlebars-data via the LSP lookup so
|
|
1784
|
+
// this stays data-driven (no hand-maintained helper list).
|
|
1785
|
+
handlebarsHelpers.push(...collectHandlebarsHelperUsages(content));
|
|
1786
|
+
}
|
|
1472
1787
|
// 3. Assess per-file difficulty
|
|
1473
|
-
const
|
|
1788
|
+
const hasCloudPagesFunction = ampFunctions.some((f) => CLOUDPAGES_ONLY_FUNCTIONS.has(f.name.toLowerCase()) &&
|
|
1474
1789
|
f.status === 'not-supported');
|
|
1475
1790
|
const hasNotMigratableSsjs = ssjsBlocks.some((b) => b.status === 'not-migratable');
|
|
1476
1791
|
const hasUnsupportedAmp = ampFunctions.some((f) => f.status === 'not-supported' &&
|
|
1477
1792
|
!CLOUDPAGES_ONLY_FUNCTIONS.has(f.name.toLowerCase()));
|
|
1478
1793
|
const hasConvertibleSsjs = ssjsBlocks.some((b) => b.status === 'needs-conversion');
|
|
1479
1794
|
const hasNeedsReview = ampFunctions.some((f) => f.status === 'needs-review');
|
|
1795
|
+
const hasHandlebarsError = handlebarsProblems.some((p) => p.severity === 'error');
|
|
1796
|
+
const hasHandlebarsWarning = handlebarsProblems.some((p) => p.severity === 'warning');
|
|
1480
1797
|
let difficulty;
|
|
1481
|
-
if (
|
|
1798
|
+
if (hasCloudPagesFunction || hasNotMigratableSsjs) {
|
|
1482
1799
|
difficulty = 'not-migratable';
|
|
1483
1800
|
}
|
|
1484
|
-
else if (hasUnsupportedAmp || hasConvertibleSsjs) {
|
|
1801
|
+
else if (hasUnsupportedAmp || hasConvertibleSsjs || hasHandlebarsError) {
|
|
1485
1802
|
difficulty = 'significant';
|
|
1486
1803
|
}
|
|
1487
|
-
else if (hasNeedsReview) {
|
|
1804
|
+
else if (hasNeedsReview || hasHandlebarsWarning) {
|
|
1488
1805
|
difficulty = 'minor';
|
|
1489
1806
|
}
|
|
1490
1807
|
else {
|
|
1491
1808
|
difficulty = 'ready';
|
|
1492
1809
|
}
|
|
1493
|
-
results.push({
|
|
1810
|
+
results.push({
|
|
1811
|
+
filename,
|
|
1812
|
+
difficulty,
|
|
1813
|
+
ampFunctions,
|
|
1814
|
+
ssjsBlocks,
|
|
1815
|
+
handlebarsProblems,
|
|
1816
|
+
handlebarsHelpers,
|
|
1817
|
+
});
|
|
1494
1818
|
}
|
|
1495
1819
|
// 4. Build Markdown report
|
|
1496
1820
|
const difficultyLabel = {
|
|
@@ -1541,22 +1865,37 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
|
|
|
1541
1865
|
}
|
|
1542
1866
|
if (result.ampFunctions.length > 0) {
|
|
1543
1867
|
fileLines.push('| Function | Line | Status | Reason |', '|---|---|---|---|');
|
|
1544
|
-
for (const
|
|
1545
|
-
const icon =
|
|
1868
|
+
for (const function_ of result.ampFunctions) {
|
|
1869
|
+
const icon = function_.status === 'supported'
|
|
1546
1870
|
? '✅'
|
|
1547
|
-
:
|
|
1871
|
+
: function_.status === 'needs-review'
|
|
1548
1872
|
? '⚠️'
|
|
1549
1873
|
: '❌';
|
|
1550
|
-
const statusLabel =
|
|
1874
|
+
const statusLabel = function_.status === 'supported'
|
|
1551
1875
|
? 'Supported'
|
|
1552
|
-
:
|
|
1876
|
+
: function_.status === 'needs-review'
|
|
1553
1877
|
? 'Needs review'
|
|
1554
1878
|
: 'Not supported';
|
|
1555
|
-
fileLines.push(`| ${
|
|
1879
|
+
fileLines.push(`| ${function_.name} | ${function_.line} | ${icon} ${statusLabel} | ${function_.reason} |`);
|
|
1556
1880
|
}
|
|
1557
1881
|
}
|
|
1558
|
-
else if (result.ssjsBlocks.length === 0
|
|
1559
|
-
|
|
1882
|
+
else if (result.ssjsBlocks.length === 0 &&
|
|
1883
|
+
result.handlebarsProblems.length === 0 &&
|
|
1884
|
+
result.handlebarsHelpers.length === 0) {
|
|
1885
|
+
fileLines.push('*No AMPscript functions, SSJS blocks, or Handlebars found.*');
|
|
1886
|
+
}
|
|
1887
|
+
if (result.handlebarsHelpers.length > 0) {
|
|
1888
|
+
fileLines.push('', '**MCN Handlebars Helpers:**', '');
|
|
1889
|
+
for (const h of result.handlebarsHelpers) {
|
|
1890
|
+
fileLines.push(`✅ \`{{${h.name}}}\` (line ${h.line}): Supported since API v${h.mcnSince}.0`);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (result.handlebarsProblems.length > 0) {
|
|
1894
|
+
fileLines.push('', '**MCN Handlebars Problems:**', '');
|
|
1895
|
+
for (const p of result.handlebarsProblems) {
|
|
1896
|
+
const icon = p.severity === 'error' ? '❌' : p.severity === 'warning' ? '⚠️' : 'ℹ️';
|
|
1897
|
+
fileLines.push(`${icon} Line ${p.line}: ${p.message}`);
|
|
1898
|
+
}
|
|
1560
1899
|
}
|
|
1561
1900
|
fileLines.push('', '---');
|
|
1562
1901
|
}
|
|
@@ -1641,7 +1980,8 @@ server.tool('rewrite_for_mcn', 'Deterministically rewrite AMPscript (and optiona
|
|
|
1641
1980
|
});
|
|
1642
1981
|
// Reassess difficulty
|
|
1643
1982
|
const hasMigratable = allNonMigratable.length > 0;
|
|
1644
|
-
const difficulty = hasMigratable &&
|
|
1983
|
+
const difficulty = hasMigratable &&
|
|
1984
|
+
allNonMigratable.some((index) => index.reason.includes('not-migratable'))
|
|
1645
1985
|
? 'not-migratable'
|
|
1646
1986
|
: ampResult.difficulty;
|
|
1647
1987
|
const result = {
|
|
@@ -1801,6 +2141,263 @@ server.tool('convertAmpscriptToSsjs', 'Deterministically convert AMPscript code
|
|
|
1801
2141
|
};
|
|
1802
2142
|
});
|
|
1803
2143
|
// ---------------------------------------------------------------------------
|
|
2144
|
+
// Tool: convertAmpscriptToHandlebars
|
|
2145
|
+
// ---------------------------------------------------------------------------
|
|
2146
|
+
server.tool('convertAmpscriptToHandlebars', 'Deterministically convert AMPscript to Marketing Cloud Next (MCN) Handlebars using the ' +
|
|
2147
|
+
'three-category model built from ampscript-data: (A) functions with a Handlebars helper ' +
|
|
2148
|
+
'equivalent become {{helper …}}; (B) functions with no MCN counterpart become a ' +
|
|
2149
|
+
'MANUAL_REWRITE_REQUIRED comment; (C) mcnHandlebarsGap functions (e.g. ContentBlockByKey) ' +
|
|
2150
|
+
'become a DISTINCT MANUAL_REWRITE_REQUIRED comment noting they are documented as ' +
|
|
2151
|
+
'MCN-supported but currently fail at runtime. Procedural AMPscript blocks (SET/VAR/IF/FOR) ' +
|
|
2152
|
+
'have no Handlebars equivalent and are flagged. ' +
|
|
2153
|
+
'Use the convertAmpscriptToHandlebars PROMPT for AI-enhanced handling of flagged sections.', {
|
|
2154
|
+
code: z.string().describe('The AMPscript code to convert to MCN Handlebars.'),
|
|
2155
|
+
}, ({ code }) => {
|
|
2156
|
+
const result = ampscriptToHandlebars(code);
|
|
2157
|
+
return {
|
|
2158
|
+
content: [
|
|
2159
|
+
{
|
|
2160
|
+
type: 'text',
|
|
2161
|
+
text: JSON.stringify({
|
|
2162
|
+
convertedCode: result.convertedCode,
|
|
2163
|
+
changes: result.changes,
|
|
2164
|
+
flaggedSections: result.flaggedSections,
|
|
2165
|
+
}, null, 2),
|
|
2166
|
+
},
|
|
2167
|
+
],
|
|
2168
|
+
};
|
|
2169
|
+
});
|
|
2170
|
+
// ---------------------------------------------------------------------------
|
|
2171
|
+
// Prompt: convertAmpscriptToHandlebars
|
|
2172
|
+
// ---------------------------------------------------------------------------
|
|
2173
|
+
server.prompt('convertAmpscriptToHandlebars', 'Convert AMPscript to Marketing Cloud Next (MCN) Handlebars. ' +
|
|
2174
|
+
'Calls the convertAmpscriptToHandlebars tool first for a deterministic, data-driven ' +
|
|
2175
|
+
'conversion, then applies AI reasoning to handle MANUAL_REWRITE_REQUIRED sections.', {
|
|
2176
|
+
code: z.string().describe('The AMPscript code to convert to MCN Handlebars.'),
|
|
2177
|
+
}, ({ code }) => ({
|
|
2178
|
+
messages: [
|
|
2179
|
+
{
|
|
2180
|
+
role: 'user',
|
|
2181
|
+
content: {
|
|
2182
|
+
type: 'text',
|
|
2183
|
+
text: [
|
|
2184
|
+
'You are an expert Salesforce Marketing Cloud developer converting AMPscript to Marketing Cloud Next (MCN) Handlebars.',
|
|
2185
|
+
'',
|
|
2186
|
+
'## Instructions',
|
|
2187
|
+
'1. Call the `convertAmpscriptToHandlebars` **tool** with the code below to get a deterministic conversion.',
|
|
2188
|
+
'2. Review the `flaggedSections` — these are constructs the tool could not convert automatically.',
|
|
2189
|
+
'3. For each flagged section, apply your expertise, but obey these hard rules:',
|
|
2190
|
+
' - NEVER invent a Handlebars helper. Only use helpers that exist in the MCN catalog (call `list_handlebars_helpers` / `lookup_handlebars_helper`).',
|
|
2191
|
+
' - Category B (no MCN counterpart): leave a clear `{{!-- MANUAL_REWRITE_REQUIRED … --}}` note explaining the manual step.',
|
|
2192
|
+
' - Category C (documented-supported but runtime gap, e.g. ContentBlockByKey): keep the DISTINCT runtime-gap note — do not replace it with a fabricated helper.',
|
|
2193
|
+
' - AMPscript procedural blocks (SET/VAR/IF/FOR) have no Handlebars equivalent — Handlebars cannot assign variables or run imperative logic. Restructure the data upstream instead.',
|
|
2194
|
+
'4. Validate your final output by calling `validate_handlebars`.',
|
|
2195
|
+
'5. Produce a single final Handlebars-in-HTML code block plus a short bulleted change log.',
|
|
2196
|
+
'',
|
|
2197
|
+
'## AMPscript code to convert',
|
|
2198
|
+
'```',
|
|
2199
|
+
code,
|
|
2200
|
+
'```',
|
|
2201
|
+
].join('\n'),
|
|
2202
|
+
},
|
|
2203
|
+
},
|
|
2204
|
+
],
|
|
2205
|
+
}));
|
|
2206
|
+
// ---------------------------------------------------------------------------
|
|
2207
|
+
// Tool: convertHandlebarsToAmpscript
|
|
2208
|
+
// ---------------------------------------------------------------------------
|
|
2209
|
+
server.tool('convertHandlebarsToAmpscript', 'Deterministically convert Marketing Cloud Next (MCN) Handlebars to AMPscript (best-effort, ' +
|
|
2210
|
+
'lossy). Inline helper calls that map back to an AMPscript function become %%=Fn(…)=%%; ' +
|
|
2211
|
+
'bare variables {{name}} become %%=v(@name)=%%. Block helpers ({{#each}}/{{#if}}), partials, ' +
|
|
2212
|
+
'dotted binding paths, and helpers with no AMPscript equivalent are flagged ' +
|
|
2213
|
+
'MANUAL_REWRITE_REQUIRED. Use the convertHandlebarsToAmpscript PROMPT for AI-enhanced handling.', {
|
|
2214
|
+
code: z.string().describe('The MCN Handlebars code to convert to AMPscript.'),
|
|
2215
|
+
}, ({ code }) => {
|
|
2216
|
+
const result = handlebarsToAmpscript(code);
|
|
2217
|
+
return {
|
|
2218
|
+
content: [
|
|
2219
|
+
{
|
|
2220
|
+
type: 'text',
|
|
2221
|
+
text: JSON.stringify({
|
|
2222
|
+
convertedCode: result.convertedCode,
|
|
2223
|
+
changes: result.changes,
|
|
2224
|
+
flaggedSections: result.flaggedSections,
|
|
2225
|
+
}, null, 2),
|
|
2226
|
+
},
|
|
2227
|
+
],
|
|
2228
|
+
};
|
|
2229
|
+
});
|
|
2230
|
+
// ---------------------------------------------------------------------------
|
|
2231
|
+
// Prompt: convertHandlebarsToAmpscript
|
|
2232
|
+
// ---------------------------------------------------------------------------
|
|
2233
|
+
server.prompt('convertHandlebarsToAmpscript', 'Convert Marketing Cloud Next (MCN) Handlebars to AMPscript (best-effort, lossy). ' +
|
|
2234
|
+
'Calls the convertHandlebarsToAmpscript tool first for a deterministic conversion, then ' +
|
|
2235
|
+
'applies AI reasoning to handle MANUAL_REWRITE_REQUIRED sections.', {
|
|
2236
|
+
code: z.string().describe('The MCN Handlebars code to convert to AMPscript.'),
|
|
2237
|
+
}, ({ code }) => ({
|
|
2238
|
+
messages: [
|
|
2239
|
+
{
|
|
2240
|
+
role: 'user',
|
|
2241
|
+
content: {
|
|
2242
|
+
type: 'text',
|
|
2243
|
+
text: [
|
|
2244
|
+
'You are an expert Salesforce Marketing Cloud developer converting MCN Handlebars to AMPscript.',
|
|
2245
|
+
'',
|
|
2246
|
+
'## Instructions',
|
|
2247
|
+
'1. Call the `convertHandlebarsToAmpscript` **tool** with the code below to get a deterministic conversion.',
|
|
2248
|
+
'2. Review the `flaggedSections` — block helpers, partials, and binding paths often need context-specific AMPscript.',
|
|
2249
|
+
'3. For each flagged section, apply your expertise:',
|
|
2250
|
+
' - `{{#each items}}…{{/each}}` → AMPscript `FOR @i = 1 TO RowCount(@items) DO … NEXT @i`',
|
|
2251
|
+
' - `{{#if cond}}…{{else}}…{{/if}}` → `IF cond THEN … ELSE … ENDIF`',
|
|
2252
|
+
' - `{!$…}` bindings and `mcn-platform` helpers may have no AMPscript equivalent — keep the MANUAL_REWRITE_REQUIRED note.',
|
|
2253
|
+
'4. Produce a single final AMPscript code block plus a short bulleted change log.',
|
|
2254
|
+
'',
|
|
2255
|
+
'## Handlebars code to convert',
|
|
2256
|
+
'```',
|
|
2257
|
+
code,
|
|
2258
|
+
'```',
|
|
2259
|
+
].join('\n'),
|
|
2260
|
+
},
|
|
2261
|
+
},
|
|
2262
|
+
],
|
|
2263
|
+
}));
|
|
2264
|
+
// ---------------------------------------------------------------------------
|
|
2265
|
+
// Tool: convertSsjsToHandlebars
|
|
2266
|
+
// ---------------------------------------------------------------------------
|
|
2267
|
+
server.tool('convertSsjsToHandlebars', 'Deterministically convert SSJS to Marketing Cloud Next (MCN) Handlebars transitively ' +
|
|
2268
|
+
'(SSJS → AMPscript → Handlebars). Because Handlebars is declarative, most imperative SSJS ' +
|
|
2269
|
+
'has no Handlebars counterpart and is conservatively flagged MANUAL_REWRITE_REQUIRED; ' +
|
|
2270
|
+
'inline Platform.Function.* calls that map through AMPscript to a Handlebars helper are ' +
|
|
2271
|
+
'converted. Use the convertSsjsToHandlebars PROMPT for AI-enhanced handling of flagged sections.', {
|
|
2272
|
+
code: z
|
|
2273
|
+
.string()
|
|
2274
|
+
.describe('The SSJS code to convert (may include <script runat="server"> tags).'),
|
|
2275
|
+
}, ({ code }) => {
|
|
2276
|
+
const result = ssjsToHandlebars(code);
|
|
2277
|
+
return {
|
|
2278
|
+
content: [
|
|
2279
|
+
{
|
|
2280
|
+
type: 'text',
|
|
2281
|
+
text: JSON.stringify({
|
|
2282
|
+
convertedCode: result.convertedCode,
|
|
2283
|
+
changes: result.changes,
|
|
2284
|
+
flaggedSections: result.flaggedSections,
|
|
2285
|
+
}, null, 2),
|
|
2286
|
+
},
|
|
2287
|
+
],
|
|
2288
|
+
};
|
|
2289
|
+
});
|
|
2290
|
+
// ---------------------------------------------------------------------------
|
|
2291
|
+
// Prompt: convertSsjsToHandlebars
|
|
2292
|
+
// ---------------------------------------------------------------------------
|
|
2293
|
+
server.prompt('convertSsjsToHandlebars', 'Convert SSJS to Marketing Cloud Next (MCN) Handlebars. Calls the convertSsjsToHandlebars ' +
|
|
2294
|
+
'tool first (SSJS → AMPscript → Handlebars), then applies AI reasoning to handle the many ' +
|
|
2295
|
+
'MANUAL_REWRITE_REQUIRED sections that imperative SSJS produces.', {
|
|
2296
|
+
code: z.string().describe('The SSJS code to convert to MCN Handlebars.'),
|
|
2297
|
+
}, ({ code }) => ({
|
|
2298
|
+
messages: [
|
|
2299
|
+
{
|
|
2300
|
+
role: 'user',
|
|
2301
|
+
content: {
|
|
2302
|
+
type: 'text',
|
|
2303
|
+
text: [
|
|
2304
|
+
'You are an expert Salesforce Marketing Cloud developer converting SSJS to Marketing Cloud Next (MCN) Handlebars.',
|
|
2305
|
+
'',
|
|
2306
|
+
'## Key reality',
|
|
2307
|
+
'MCN runs a locked-down Handlebars engine. SSJS does NOT exist in MCN, and Handlebars is declarative — it cannot run imperative logic. Conversion is only possible transitively (SSJS → MCN-valid AMPscript subset → Handlebars), and most non-trivial SSJS will need a redesign (move logic to the data layer / a query).',
|
|
2308
|
+
'',
|
|
2309
|
+
'## Instructions',
|
|
2310
|
+
'1. Call the `convertSsjsToHandlebars` **tool** with the code below to get a deterministic conversion.',
|
|
2311
|
+
'2. Review the `flaggedSections` from BOTH stages ([SSJS→AMPscript] and [AMPscript→Handlebars]).',
|
|
2312
|
+
'3. For each flagged section, apply your expertise — but NEVER invent a Handlebars helper (use only the MCN catalog via `list_handlebars_helpers`).',
|
|
2313
|
+
'4. Where imperative logic cannot be expressed, recommend moving it upstream (e.g. a `{{#query}}` / data binding) and keep a clear MANUAL_REWRITE_REQUIRED note.',
|
|
2314
|
+
'5. Validate your final output by calling `validate_handlebars`.',
|
|
2315
|
+
'6. Produce a single final Handlebars-in-HTML code block plus a short bulleted change log.',
|
|
2316
|
+
'',
|
|
2317
|
+
'## SSJS code to convert',
|
|
2318
|
+
'```javascript',
|
|
2319
|
+
code,
|
|
2320
|
+
'```',
|
|
2321
|
+
].join('\n'),
|
|
2322
|
+
},
|
|
2323
|
+
},
|
|
2324
|
+
],
|
|
2325
|
+
}));
|
|
2326
|
+
// ---------------------------------------------------------------------------
|
|
2327
|
+
// Tool: write_handlebars
|
|
2328
|
+
// ---------------------------------------------------------------------------
|
|
2329
|
+
server.tool('write_handlebars', 'Validate a Marketing Cloud Next (MCN) Handlebars-in-HTML draft so it can be finalized as ' +
|
|
2330
|
+
'authored content. Runs the MCN Handlebars validator (locked-down engine) on the draft and ' +
|
|
2331
|
+
'returns whether it is clean, plus any diagnostics (unknown/too-new helpers, unsupported ' +
|
|
2332
|
+
'constructs, arity). Use the write_handlebars PROMPT to AUTHOR from intent — that prompt ' +
|
|
2333
|
+
'instructs the model to use only catalog helpers and then call this tool to verify.', {
|
|
2334
|
+
draft: z.string().describe('The Handlebars-in-HTML draft to validate before finalizing.'),
|
|
2335
|
+
intent: z
|
|
2336
|
+
.string()
|
|
2337
|
+
.optional()
|
|
2338
|
+
.describe('Optional human description of what the content should do (echoed for context).'),
|
|
2339
|
+
}, ({ draft, intent }) => {
|
|
2340
|
+
const diagnostics = validateAmpscript(draft, {
|
|
2341
|
+
maxNumberOfProblems: 100,
|
|
2342
|
+
targetPlatform: 'next',
|
|
2343
|
+
}).filter((d) => d.source === 'handlebars');
|
|
2344
|
+
const isClean = diagnostics.length === 0;
|
|
2345
|
+
const header = isClean
|
|
2346
|
+
? '✅ Draft is valid MCN Handlebars.'
|
|
2347
|
+
: `❌ Draft has ${diagnostics.length} MCN Handlebars issue(s) — fix before finalizing:`;
|
|
2348
|
+
const intentLine = intent ? `Intent: ${intent}\n\n` : '';
|
|
2349
|
+
return {
|
|
2350
|
+
content: [
|
|
2351
|
+
{
|
|
2352
|
+
type: 'text',
|
|
2353
|
+
text: `${intentLine}${header}\n\n${formatDiagnostics(diagnostics)}`,
|
|
2354
|
+
},
|
|
2355
|
+
],
|
|
2356
|
+
};
|
|
2357
|
+
});
|
|
2358
|
+
// ---------------------------------------------------------------------------
|
|
2359
|
+
// Prompt: writeHandlebars
|
|
2360
|
+
// ---------------------------------------------------------------------------
|
|
2361
|
+
server.prompt('writeHandlebars', 'Author Marketing Cloud Next (MCN) Handlebars-in-HTML from a natural-language intent, using ' +
|
|
2362
|
+
'only helpers that exist in the MCN catalog, then validate the result with write_handlebars.', {
|
|
2363
|
+
intent: z
|
|
2364
|
+
.string()
|
|
2365
|
+
.describe('What the content should do, e.g. "greet the subscriber by first name and show their loyalty tier".'),
|
|
2366
|
+
context: z
|
|
2367
|
+
.string()
|
|
2368
|
+
.optional()
|
|
2369
|
+
.describe('Optional data/personalization context (available fields, bindings, sample data).'),
|
|
2370
|
+
}, ({ intent, context }) => ({
|
|
2371
|
+
messages: [
|
|
2372
|
+
{
|
|
2373
|
+
role: 'user',
|
|
2374
|
+
content: {
|
|
2375
|
+
type: 'text',
|
|
2376
|
+
text: [
|
|
2377
|
+
'You are an expert Salesforce Marketing Cloud developer authoring Marketing Cloud Next (MCN) Handlebars-in-HTML.',
|
|
2378
|
+
'',
|
|
2379
|
+
'## Hard rules',
|
|
2380
|
+
'- MCN runs a LOCKED-DOWN Handlebars engine. NEVER invent a helper. Only use helpers in the MCN catalog — call `list_handlebars_helpers` (and `lookup_handlebars_helper` for signatures) before writing.',
|
|
2381
|
+
'- Respect each helper`s `mcnSince` availability.',
|
|
2382
|
+
'- Partials ({{> …}}), decorators ({{* …}}), and built-in helpers absent from MCN (e.g. {{log}}) are NOT allowed.',
|
|
2383
|
+
'- `{!$…}` bindings are merge fields, not helpers — use `list_handlebars_helpers` output / bindings catalog for valid ones.',
|
|
2384
|
+
'',
|
|
2385
|
+
'## Instructions',
|
|
2386
|
+
'1. Inspect the available catalog with `list_handlebars_helpers`.',
|
|
2387
|
+
'2. Author the Handlebars-in-HTML to satisfy the intent below.',
|
|
2388
|
+
'3. Call the `write_handlebars` **tool** with your draft to validate it.',
|
|
2389
|
+
'4. If validation reports issues, fix them and re-validate until clean.',
|
|
2390
|
+
'5. Return the final, validated Handlebars-in-HTML code block.',
|
|
2391
|
+
'',
|
|
2392
|
+
`## Intent`,
|
|
2393
|
+
intent,
|
|
2394
|
+
...(context ? ['', '## Context', context] : []),
|
|
2395
|
+
].join('\n'),
|
|
2396
|
+
},
|
|
2397
|
+
},
|
|
2398
|
+
],
|
|
2399
|
+
}));
|
|
2400
|
+
// ---------------------------------------------------------------------------
|
|
1804
2401
|
// Tool: get_server_version
|
|
1805
2402
|
// ---------------------------------------------------------------------------
|
|
1806
2403
|
server.tool('get_server_version', 'Return the running mcp-server-sfmc version and the size of the bundled Salesforce ' +
|
|
@@ -1815,7 +2412,7 @@ server.tool('get_server_version', 'Return the running mcp-server-sfmc version an
|
|
|
1815
2412
|
type: 'text',
|
|
1816
2413
|
text: JSON.stringify({
|
|
1817
2414
|
name: 'mcp-server-sfmc',
|
|
1818
|
-
version:
|
|
2415
|
+
version: package_.version,
|
|
1819
2416
|
mceHelp: { chunkCount: mce.chunkCount, fileCount: mceFileCount },
|
|
1820
2417
|
mcnHelp: { chunkCount: mcn.chunkCount, fileCount: mcn.fileCount },
|
|
1821
2418
|
}, null, 2),
|
|
@@ -1828,15 +2425,18 @@ server.tool('get_server_version', 'Return the running mcp-server-sfmc version an
|
|
|
1828
2425
|
// ---------------------------------------------------------------------------
|
|
1829
2426
|
async function main() {
|
|
1830
2427
|
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
1831
|
-
process.stdout.write(
|
|
2428
|
+
process.stdout.write(package_.version + '\n');
|
|
1832
2429
|
return;
|
|
1833
2430
|
}
|
|
1834
2431
|
const transport = new StdioServerTransport();
|
|
1835
2432
|
await server.connect(transport);
|
|
1836
2433
|
process.stderr.write('mcp-server-sfmc running on stdio\n');
|
|
1837
2434
|
}
|
|
1838
|
-
|
|
2435
|
+
try {
|
|
2436
|
+
await main();
|
|
2437
|
+
}
|
|
2438
|
+
catch (ex) {
|
|
1839
2439
|
process.stderr.write(`Fatal: ${String(ex)}\n`);
|
|
1840
2440
|
process.exit(1);
|
|
1841
|
-
}
|
|
2441
|
+
}
|
|
1842
2442
|
//# sourceMappingURL=index.js.map
|