mcp-server-sfmc 1.9.1 → 2.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/dist/index.js CHANGED
@@ -16,7 +16,7 @@ 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)), '..');
@@ -118,6 +118,28 @@ function formatDiagnostics(diagnostics) {
118
118
  })
119
119
  .join('\n');
120
120
  }
121
+ function formatHandlebarsHelper(helper) {
122
+ const params = helper.params
123
+ .map((p) => {
124
+ const req = p.optional ? '(optional)' : '(required)';
125
+ const variadic = p.variadic ? ' (variadic)' : '';
126
+ return ` - ${p.name}: ${p.type}${variadic} ${req}${p.description ? ' — ' + p.description : ''}`;
127
+ })
128
+ .join('\n');
129
+ const subexpr = helper.subexpressionOnly
130
+ ? '\n\n> Subexpression-only — may only be used inside `( … )`.'
131
+ : '';
132
+ return (`## {{${helper.name}}}\n\n` +
133
+ `**Category:** ${helper.category}\n\n` +
134
+ `**Origin:** ${helper.origin}\n\n` +
135
+ `**Type:** ${helper.helperType}\n\n` +
136
+ `**Returns:** ${helper.returnType}\n\n` +
137
+ `**Description:** ${helper.description}\n\n` +
138
+ `**Parameters:**\n${params || ' (none)'}` +
139
+ subexpr +
140
+ `\n\n✅ **Marketing Cloud Next:** Supported since API v${helper.mcnSince}.0` +
141
+ (helper.docUrl ? `\n\n[Documentation](${helper.docUrl})` : ''));
142
+ }
121
143
  // ---------------------------------------------------------------------------
122
144
  // Tool: validate_ampscript
123
145
  // ---------------------------------------------------------------------------
@@ -289,6 +311,90 @@ server.tool('list_ampscript_functions', 'List all AMPscript functions, optionall
289
311
  };
290
312
  });
291
313
  // ---------------------------------------------------------------------------
314
+ // Tool: validate_handlebars
315
+ // ---------------------------------------------------------------------------
316
+ server.tool('validate_handlebars', 'Validate Marketing Cloud Next (MCN) Handlebars template code. ' +
317
+ 'Checks helper names, arity, block balance, and flags unsupported constructs ' +
318
+ '(partials, decorators, and built-in helpers absent from the locked-down MCN engine). ' +
319
+ 'MCN Handlebars lives inside the combined sfmc language and is always validated for the ' +
320
+ "'next' target.", {
321
+ code: z
322
+ .string()
323
+ .describe('The MCN Handlebars template code to validate. May include HTML context.'),
324
+ maxProblems: z
325
+ .number()
326
+ .int()
327
+ .min(1)
328
+ .max(500)
329
+ .optional()
330
+ .describe('Maximum number of problems to return (default 100).'),
331
+ }, ({ code, maxProblems }) => {
332
+ const settings = {
333
+ maxNumberOfProblems: maxProblems ?? 100,
334
+ targetPlatform: 'next',
335
+ };
336
+ const diagnostics = validateAmpscript(code, settings).filter((d) => d.source === 'handlebars');
337
+ return {
338
+ content: [{ type: 'text', text: formatDiagnostics(diagnostics) }],
339
+ };
340
+ });
341
+ // ---------------------------------------------------------------------------
342
+ // Tool: lookup_handlebars_helper
343
+ // ---------------------------------------------------------------------------
344
+ server.tool('lookup_handlebars_helper', 'Look up the signature, parameters, description, and origin for a Marketing Cloud Next ' +
345
+ 'Handlebars helper by name. Case-insensitive. Returns null if the helper is not found.', {
346
+ name: z
347
+ .string()
348
+ .describe('The Handlebars helper name, e.g. "uppercase", "if", "formatDate".'),
349
+ }, ({ name }) => {
350
+ const helper = sfmcLanguageService.lookupHandlebarsHelper(name);
351
+ if (!helper) {
352
+ return {
353
+ content: [{ type: 'text', text: `Handlebars helper "${name}" not found.` }],
354
+ };
355
+ }
356
+ return { content: [{ type: 'text', text: formatHandlebarsHelper(helper) }] };
357
+ });
358
+ // ---------------------------------------------------------------------------
359
+ // Tool: list_handlebars_helpers
360
+ // ---------------------------------------------------------------------------
361
+ server.tool('list_handlebars_helpers', 'List all Marketing Cloud Next Handlebars helpers, optionally filtered by category ' +
362
+ 'and/or origin (handlebars-builtin, mcn-helper, mcn-platform).', {
363
+ category: z
364
+ .string()
365
+ .optional()
366
+ .describe('Filter by helper category (case-insensitive substring match), e.g. "string", "date", "comparison".'),
367
+ origin: z
368
+ .enum(['handlebars-builtin', 'mcn-helper', 'mcn-platform'])
369
+ .optional()
370
+ .describe('Filter by helper origin.'),
371
+ }, ({ category, origin }) => {
372
+ let helpers = sfmcLanguageService.listHandlebarsHelpers();
373
+ if (origin) {
374
+ helpers = helpers.filter((h) => h.origin === origin);
375
+ }
376
+ if (category) {
377
+ const catLower = category.toLowerCase();
378
+ helpers = helpers.filter((h) => h.category.toLowerCase().includes(catLower));
379
+ }
380
+ if (helpers.length === 0) {
381
+ return {
382
+ content: [{ type: 'text', text: 'No Handlebars helpers match the filter.' }],
383
+ };
384
+ }
385
+ const rows = helpers
386
+ .map((h) => `- **${h.name}** *(${h.category}, ${h.origin}, ${h.helperType})*: ${h.description}`)
387
+ .join('\n');
388
+ return {
389
+ content: [
390
+ {
391
+ type: 'text',
392
+ text: `## MCN Handlebars Helpers\n\n${rows}`,
393
+ },
394
+ ],
395
+ };
396
+ });
397
+ // ---------------------------------------------------------------------------
292
398
  // ECMAScript-builtin support lookup, sourced exclusively from ssjs-data.
293
399
  // Used as a fall-through inside lookup_ssjs_function so a single tool answers
294
400
  // "can I use X in SSJS?" for both SFMC APIs and plain-JavaScript built-ins.
@@ -568,6 +674,24 @@ server.tool('suggest_fix', 'Generate a corrected version of SFMC code based on v
568
674
  */
569
675
  function getFixSuggestion(message, line, lang) {
570
676
  const m = message.toLowerCase();
677
+ // MCN Handlebars diagnostics (surfaced when target === 'next' on {{…}} regions).
678
+ if (m.includes('partials ({{>'))
679
+ return 'Inline the partial content directly — the locked-down MCN engine cannot register partials.';
680
+ if (m.includes('partial blocks'))
681
+ return 'Inline the block content directly — the MCN engine cannot register partial blocks.';
682
+ if (m.includes('decorators ({{*') || m.includes('decorator blocks'))
683
+ return 'Remove the decorator — the MCN engine cannot register decorators.';
684
+ if (m.includes('{{log}}'))
685
+ return 'Remove the {{log}} debugging helper — it is not available in MCN Handlebars.';
686
+ if (m.includes('unknown handlebars helper')) {
687
+ const helperMatch = message.match(/'([^']+)'/);
688
+ const hint = helperMatch
689
+ ? ` Check spelling or use a catalog helper instead of "${helperMatch[1]}".`
690
+ : '';
691
+ return `Use a helper from the MCN Handlebars catalog (list_handlebars_helpers); custom helpers cannot be registered.${hint}`;
692
+ }
693
+ if (m.includes('handlebars for marketing cloud next') || m.includes('mcn engine'))
694
+ return 'Replace with a supported MCN Handlebars construct — see list_handlebars_helpers.';
571
695
  if (m.includes("'let'") || m.includes("'const'"))
572
696
  return 'Replace `let`/`const` with `var`.';
573
697
  if (m.includes('arrow function'))
@@ -693,12 +817,81 @@ server.tool('get_ssjs_completions', 'Return a list of SSJS Platform functions, W
693
817
  };
694
818
  });
695
819
  // ---------------------------------------------------------------------------
820
+ // Tool: get_handlebars_completions
821
+ // ---------------------------------------------------------------------------
822
+ server.tool('get_handlebars_completions', 'Return Marketing Cloud Next (MCN) Handlebars helper completions with snippet insert text. ' +
823
+ 'MCN Handlebars is only available on the next platform, so this catalog is always the ' +
824
+ '\'next\' set. Optionally filter by a name prefix (e.g. "form", "date").', {
825
+ filter: z
826
+ .string()
827
+ .optional()
828
+ .describe('Optional helper-name prefix filter (case-insensitive), e.g. "form", "to".'),
829
+ }, ({ filter }) => {
830
+ const items = sfmcLanguageService.getHandlebarsCompletionCatalog();
831
+ const filtered = filter
832
+ ? items.filter((item) => {
833
+ const label = typeof item.label === 'string'
834
+ ? item.label
835
+ : item.label.label;
836
+ return label.toLowerCase().startsWith(filter.toLowerCase());
837
+ })
838
+ : items;
839
+ const formatted = filtered
840
+ .slice(0, 80)
841
+ .map((item) => {
842
+ const label = typeof item.label === 'string'
843
+ ? item.label
844
+ : item.label.label;
845
+ return `- ${label}${item.detail ? ` — ${item.detail}` : ''}`;
846
+ })
847
+ .join('\n');
848
+ return {
849
+ content: [
850
+ {
851
+ type: 'text',
852
+ text: filtered.length === 0
853
+ ? `No MCN Handlebars helpers matching "${filter}".`
854
+ : `${filtered.length} MCN Handlebars helper completions${filter ? ` matching "${filter}"` : ''} (showing up to 80):\n\n${formatted}`,
855
+ },
856
+ ],
857
+ };
858
+ });
859
+ // ---------------------------------------------------------------------------
696
860
  // Tool: format_sfmc_code (basic, no prettier integration needed)
697
861
  // ---------------------------------------------------------------------------
698
- server.tool('format_sfmc_code', 'Apply basic formatting conventions to AMPscript or SSJS code. ' +
699
- 'Normalises keyword casing, whitespace around operators, and indentation hints.', {
862
+ /**
863
+ * Conservatively normalize whitespace inside Marketing Cloud Next (MCN)
864
+ * Handlebars `{{…}}` mustaches: collapse runs of internal whitespace to a
865
+ * single space and trim the edges (e.g. `{{ foo bar }}` → `{{foo bar}}`),
866
+ * matching what Prettier's Glimmer parser does for mustache interiors.
867
+ *
868
+ * Hard guardrails (never altered):
869
+ * - `{!$…}` merge-field bindings — they are not Handlebars syntax.
870
+ * - `{{!-- … --}}` / `{{! … }}` comments — preserved byte-for-byte.
871
+ *
872
+ * This is the deferred-Prettier stub for item 8 of the MCN Handlebars handoff:
873
+ * full routing through prettier-plugin-sfmc's Glimmer path lands once that
874
+ * plugin ships Handlebars support (see HANDOFF-prettier-handlebars.md).
875
+ * @param {string} code - The Handlebars-in-HTML code to normalize.
876
+ * @returns {string} The code with mustache interiors normalized.
877
+ */
878
+ function normalizeHandlebarsWhitespace(code) {
879
+ return code.replaceAll(/\{\{([\s\S]*?)\}\}/g, (full, inner) => {
880
+ // Preserve comments verbatim.
881
+ if (inner.startsWith('!')) {
882
+ return full;
883
+ }
884
+ const normalized = inner.replaceAll(/\s+/g, ' ').trim();
885
+ return `{{${normalized}}}`;
886
+ });
887
+ }
888
+ server.tool('format_sfmc_code', 'Apply basic formatting conventions to AMPscript, SSJS, or Marketing Cloud Next (MCN) ' +
889
+ 'Handlebars code. Normalises keyword casing and whitespace. For Handlebars, collapses ' +
890
+ 'whitespace inside {{…}} mustaches while leaving {!$…} bindings and {{!-- … --}} comments ' +
891
+ 'untouched. (Handlebars formatting is a conservative whitespace normalizer; full Prettier ' +
892
+ 'Glimmer routing is deferred until prettier-plugin-sfmc ships Handlebars support.)', {
700
893
  code: z.string().describe('The SFMC code to format.'),
701
- language: z.enum(['ampscript', 'ssjs']).describe('The language of the code.'),
894
+ language: z.enum(['ampscript', 'ssjs', 'handlebars']).describe('The language of the code.'),
702
895
  }, ({ code, language }) => {
703
896
  let formatted = code;
704
897
  if (language === 'ampscript') {
@@ -709,6 +902,9 @@ server.tool('format_sfmc_code', 'Apply basic formatting conventions to AMPscript
709
902
  .replaceAll(/\bOR\b/gi, 'OR')
710
903
  .replaceAll(/\bNOT\b/gi, 'NOT');
711
904
  }
905
+ else if (language === 'handlebars') {
906
+ formatted = normalizeHandlebarsWhitespace(formatted);
907
+ }
712
908
  else {
713
909
  // SSJS: normalise Platform.Load to use double quotes
714
910
  formatted = formatted.replaceAll(/Platform\.Load\s*\(\s*'([^']*)'\s*,\s*'([^']*)'\s*\)/g, 'Platform.Load("$1", "$2")');
@@ -817,6 +1013,48 @@ server.resource('ssjs-function-catalog', 'sfmc://ssjs/functions', async () => {
817
1013
  };
818
1014
  });
819
1015
  // ---------------------------------------------------------------------------
1016
+ // Resource: handlebars-helper-catalog
1017
+ // ---------------------------------------------------------------------------
1018
+ server.resource('handlebars-helper-catalog', 'sfmc://handlebars/helpers', async () => {
1019
+ const helpers = sfmcLanguageService.listHandlebarsHelpers();
1020
+ const lines = helpers.map((h) => {
1021
+ const paramList = h.params
1022
+ .map((p) => {
1023
+ const inner = `${p.name}: ${p.type}`;
1024
+ return p.optional ? `[${inner}]` : inner;
1025
+ })
1026
+ .join(', ');
1027
+ return `{{${h.name} ${paramList}}} — (${h.category}, ${h.origin}, v${h.mcnSince}.0+) ${h.description}`;
1028
+ });
1029
+ return {
1030
+ contents: [
1031
+ {
1032
+ uri: 'sfmc://handlebars/helpers',
1033
+ mimeType: 'text/plain',
1034
+ text: `# MCN Handlebars Helper Catalog (${helpers.length} helpers)\n\n` +
1035
+ lines.join('\n'),
1036
+ },
1037
+ ],
1038
+ };
1039
+ });
1040
+ // ---------------------------------------------------------------------------
1041
+ // Resource: handlebars-binding-catalog
1042
+ // ---------------------------------------------------------------------------
1043
+ server.resource('handlebars-binding-catalog', 'sfmc://handlebars/bindings', async () => {
1044
+ const bindings = sfmcLanguageService.listHandlebarsBindings();
1045
+ const lines = bindings.map((b) => `${b.token} — (${b.namespace}, v${b.mcnSince}.0+) ${b.description}`);
1046
+ return {
1047
+ contents: [
1048
+ {
1049
+ uri: 'sfmc://handlebars/bindings',
1050
+ mimeType: 'text/plain',
1051
+ text: `# MCN Handlebars Built-in Binding Catalog (${bindings.length} bindings)\n\n` +
1052
+ lines.join('\n'),
1053
+ },
1054
+ ],
1055
+ };
1056
+ });
1057
+ // ---------------------------------------------------------------------------
820
1058
  // Resource: ampscript-keywords
821
1059
  // ---------------------------------------------------------------------------
822
1060
  server.resource('ampscript-keywords', 'sfmc://ampscript/keywords', async () => {
@@ -1417,7 +1655,15 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
1417
1655
  const notes = getMcnNotes(site.name);
1418
1656
  let status;
1419
1657
  let reason;
1420
- if (mcnSince !== null && notes === null) {
1658
+ const isHbsGap = AMP_MCN_HANDLEBARS_GAP.has(site.name.toLowerCase());
1659
+ if (isHbsGap) {
1660
+ // Category C: documented as MCN-supported but no working Handlebars
1661
+ // helper exists yet — fails at runtime. Always needs-review, even when
1662
+ // mcnSince is set, because the data flags a runtime gap.
1663
+ status = 'needs-review';
1664
+ reason = `${site.name}() is ${HBS_GAP_NOTE}`;
1665
+ }
1666
+ else if (mcnSince !== null && notes === null) {
1421
1667
  status = 'supported';
1422
1668
  reason = '—';
1423
1669
  }
@@ -1469,6 +1715,46 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
1469
1715
  });
1470
1716
  }
1471
1717
  }
1718
+ // 2b. Analyze MCN Handlebars regions (only meaningful for the 'next' target).
1719
+ // Diagnostics flag unsupported constructs (partials, decorators, built-in
1720
+ // helpers absent from the locked-down engine), unknown helpers, and arity.
1721
+ const handlebarsProblems = [];
1722
+ const handlebarsHelpers = [];
1723
+ if (content.includes('{{')) {
1724
+ const hbsDiagnostics = validateAmpscript(content, {
1725
+ maxNumberOfProblems: 200,
1726
+ targetPlatform: 'next',
1727
+ }).filter((d) => d.source === 'handlebars');
1728
+ for (const d of hbsDiagnostics) {
1729
+ const severity = d.severity === 1 ? 'error' : d.severity === 2 ? 'warning' : 'info';
1730
+ const message = typeof d.message === 'string' ? d.message : d.message.value;
1731
+ handlebarsProblems.push({
1732
+ line: d.range.start.line + 1,
1733
+ severity,
1734
+ message,
1735
+ });
1736
+ }
1737
+ // Report recognized helper usages with their MCN availability version.
1738
+ // Helper names are matched against handlebars-data via the LSP lookup so
1739
+ // this stays data-driven (no hand-maintained helper list).
1740
+ const helperCallPattern = /\{\{[#/]?\s*([a-zA-Z][\w-]*)/g;
1741
+ const seenHelper = new Set();
1742
+ let helperMatch;
1743
+ while ((helperMatch = helperCallPattern.exec(content)) !== null) {
1744
+ const token = helperMatch[1];
1745
+ const helper = sfmcLanguageService.lookupHandlebarsHelper(token);
1746
+ if (!helper) {
1747
+ continue;
1748
+ }
1749
+ const line = content.slice(0, helperMatch.index).split('\n').length;
1750
+ const dedupeKey = `${helper.name}@${line}`;
1751
+ if (seenHelper.has(dedupeKey)) {
1752
+ continue;
1753
+ }
1754
+ seenHelper.add(dedupeKey);
1755
+ handlebarsHelpers.push({ name: helper.name, line, mcnSince: helper.mcnSince });
1756
+ }
1757
+ }
1472
1758
  // 3. Assess per-file difficulty
1473
1759
  const hasCloudPagesFn = ampFunctions.some((f) => CLOUDPAGES_ONLY_FUNCTIONS.has(f.name.toLowerCase()) &&
1474
1760
  f.status === 'not-supported');
@@ -1477,20 +1763,29 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
1477
1763
  !CLOUDPAGES_ONLY_FUNCTIONS.has(f.name.toLowerCase()));
1478
1764
  const hasConvertibleSsjs = ssjsBlocks.some((b) => b.status === 'needs-conversion');
1479
1765
  const hasNeedsReview = ampFunctions.some((f) => f.status === 'needs-review');
1766
+ const hasHandlebarsError = handlebarsProblems.some((p) => p.severity === 'error');
1767
+ const hasHandlebarsWarning = handlebarsProblems.some((p) => p.severity === 'warning');
1480
1768
  let difficulty;
1481
1769
  if (hasCloudPagesFn || hasNotMigratableSsjs) {
1482
1770
  difficulty = 'not-migratable';
1483
1771
  }
1484
- else if (hasUnsupportedAmp || hasConvertibleSsjs) {
1772
+ else if (hasUnsupportedAmp || hasConvertibleSsjs || hasHandlebarsError) {
1485
1773
  difficulty = 'significant';
1486
1774
  }
1487
- else if (hasNeedsReview) {
1775
+ else if (hasNeedsReview || hasHandlebarsWarning) {
1488
1776
  difficulty = 'minor';
1489
1777
  }
1490
1778
  else {
1491
1779
  difficulty = 'ready';
1492
1780
  }
1493
- results.push({ filename, difficulty, ampFunctions, ssjsBlocks });
1781
+ results.push({
1782
+ filename,
1783
+ difficulty,
1784
+ ampFunctions,
1785
+ ssjsBlocks,
1786
+ handlebarsProblems,
1787
+ handlebarsHelpers,
1788
+ });
1494
1789
  }
1495
1790
  // 4. Build Markdown report
1496
1791
  const difficultyLabel = {
@@ -1555,8 +1850,23 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
1555
1850
  fileLines.push(`| ${fn.name} | ${fn.line} | ${icon} ${statusLabel} | ${fn.reason} |`);
1556
1851
  }
1557
1852
  }
1558
- else if (result.ssjsBlocks.length === 0) {
1559
- fileLines.push('*No AMPscript functions or SSJS blocks found.*');
1853
+ else if (result.ssjsBlocks.length === 0 &&
1854
+ result.handlebarsProblems.length === 0 &&
1855
+ result.handlebarsHelpers.length === 0) {
1856
+ fileLines.push('*No AMPscript functions, SSJS blocks, or Handlebars found.*');
1857
+ }
1858
+ if (result.handlebarsHelpers.length > 0) {
1859
+ fileLines.push('', '**MCN Handlebars Helpers:**', '');
1860
+ for (const h of result.handlebarsHelpers) {
1861
+ fileLines.push(`✅ \`{{${h.name}}}\` (line ${h.line}): Supported since API v${h.mcnSince}.0`);
1862
+ }
1863
+ }
1864
+ if (result.handlebarsProblems.length > 0) {
1865
+ fileLines.push('', '**MCN Handlebars Problems:**', '');
1866
+ for (const p of result.handlebarsProblems) {
1867
+ const icon = p.severity === 'error' ? '❌' : p.severity === 'warning' ? '⚠️' : 'ℹ️';
1868
+ fileLines.push(`${icon} Line ${p.line}: ${p.message}`);
1869
+ }
1560
1870
  }
1561
1871
  fileLines.push('', '---');
1562
1872
  }
@@ -1801,6 +2111,263 @@ server.tool('convertAmpscriptToSsjs', 'Deterministically convert AMPscript code
1801
2111
  };
1802
2112
  });
1803
2113
  // ---------------------------------------------------------------------------
2114
+ // Tool: convertAmpscriptToHandlebars
2115
+ // ---------------------------------------------------------------------------
2116
+ server.tool('convertAmpscriptToHandlebars', 'Deterministically convert AMPscript to Marketing Cloud Next (MCN) Handlebars using the ' +
2117
+ 'three-category model built from ampscript-data: (A) functions with a Handlebars helper ' +
2118
+ 'equivalent become {{helper …}}; (B) functions with no MCN counterpart become a ' +
2119
+ 'MANUAL_REWRITE_REQUIRED comment; (C) mcnHandlebarsGap functions (e.g. ContentBlockByKey) ' +
2120
+ 'become a DISTINCT MANUAL_REWRITE_REQUIRED comment noting they are documented as ' +
2121
+ 'MCN-supported but currently fail at runtime. Procedural AMPscript blocks (SET/VAR/IF/FOR) ' +
2122
+ 'have no Handlebars equivalent and are flagged. ' +
2123
+ 'Use the convertAmpscriptToHandlebars PROMPT for AI-enhanced handling of flagged sections.', {
2124
+ code: z.string().describe('The AMPscript code to convert to MCN Handlebars.'),
2125
+ }, ({ code }) => {
2126
+ const result = ampscriptToHandlebars(code);
2127
+ return {
2128
+ content: [
2129
+ {
2130
+ type: 'text',
2131
+ text: JSON.stringify({
2132
+ convertedCode: result.convertedCode,
2133
+ changes: result.changes,
2134
+ flaggedSections: result.flaggedSections,
2135
+ }, null, 2),
2136
+ },
2137
+ ],
2138
+ };
2139
+ });
2140
+ // ---------------------------------------------------------------------------
2141
+ // Prompt: convertAmpscriptToHandlebars
2142
+ // ---------------------------------------------------------------------------
2143
+ server.prompt('convertAmpscriptToHandlebars', 'Convert AMPscript to Marketing Cloud Next (MCN) Handlebars. ' +
2144
+ 'Calls the convertAmpscriptToHandlebars tool first for a deterministic, data-driven ' +
2145
+ 'conversion, then applies AI reasoning to handle MANUAL_REWRITE_REQUIRED sections.', {
2146
+ code: z.string().describe('The AMPscript code to convert to MCN Handlebars.'),
2147
+ }, ({ code }) => ({
2148
+ messages: [
2149
+ {
2150
+ role: 'user',
2151
+ content: {
2152
+ type: 'text',
2153
+ text: [
2154
+ 'You are an expert Salesforce Marketing Cloud developer converting AMPscript to Marketing Cloud Next (MCN) Handlebars.',
2155
+ '',
2156
+ '## Instructions',
2157
+ '1. Call the `convertAmpscriptToHandlebars` **tool** with the code below to get a deterministic conversion.',
2158
+ '2. Review the `flaggedSections` — these are constructs the tool could not convert automatically.',
2159
+ '3. For each flagged section, apply your expertise, but obey these hard rules:',
2160
+ ' - NEVER invent a Handlebars helper. Only use helpers that exist in the MCN catalog (call `list_handlebars_helpers` / `lookup_handlebars_helper`).',
2161
+ ' - Category B (no MCN counterpart): leave a clear `{{!-- MANUAL_REWRITE_REQUIRED … --}}` note explaining the manual step.',
2162
+ ' - Category C (documented-supported but runtime gap, e.g. ContentBlockByKey): keep the DISTINCT runtime-gap note — do not replace it with a fabricated helper.',
2163
+ ' - AMPscript procedural blocks (SET/VAR/IF/FOR) have no Handlebars equivalent — Handlebars cannot assign variables or run imperative logic. Restructure the data upstream instead.',
2164
+ '4. Validate your final output by calling `validate_handlebars`.',
2165
+ '5. Produce a single final Handlebars-in-HTML code block plus a short bulleted change log.',
2166
+ '',
2167
+ '## AMPscript code to convert',
2168
+ '```',
2169
+ code,
2170
+ '```',
2171
+ ].join('\n'),
2172
+ },
2173
+ },
2174
+ ],
2175
+ }));
2176
+ // ---------------------------------------------------------------------------
2177
+ // Tool: convertHandlebarsToAmpscript
2178
+ // ---------------------------------------------------------------------------
2179
+ server.tool('convertHandlebarsToAmpscript', 'Deterministically convert Marketing Cloud Next (MCN) Handlebars to AMPscript (best-effort, ' +
2180
+ 'lossy). Inline helper calls that map back to an AMPscript function become %%=Fn(…)=%%; ' +
2181
+ 'bare variables {{name}} become %%=v(@name)=%%. Block helpers ({{#each}}/{{#if}}), partials, ' +
2182
+ 'dotted binding paths, and helpers with no AMPscript equivalent are flagged ' +
2183
+ 'MANUAL_REWRITE_REQUIRED. Use the convertHandlebarsToAmpscript PROMPT for AI-enhanced handling.', {
2184
+ code: z.string().describe('The MCN Handlebars code to convert to AMPscript.'),
2185
+ }, ({ code }) => {
2186
+ const result = handlebarsToAmpscript(code);
2187
+ return {
2188
+ content: [
2189
+ {
2190
+ type: 'text',
2191
+ text: JSON.stringify({
2192
+ convertedCode: result.convertedCode,
2193
+ changes: result.changes,
2194
+ flaggedSections: result.flaggedSections,
2195
+ }, null, 2),
2196
+ },
2197
+ ],
2198
+ };
2199
+ });
2200
+ // ---------------------------------------------------------------------------
2201
+ // Prompt: convertHandlebarsToAmpscript
2202
+ // ---------------------------------------------------------------------------
2203
+ server.prompt('convertHandlebarsToAmpscript', 'Convert Marketing Cloud Next (MCN) Handlebars to AMPscript (best-effort, lossy). ' +
2204
+ 'Calls the convertHandlebarsToAmpscript tool first for a deterministic conversion, then ' +
2205
+ 'applies AI reasoning to handle MANUAL_REWRITE_REQUIRED sections.', {
2206
+ code: z.string().describe('The MCN Handlebars code to convert to AMPscript.'),
2207
+ }, ({ code }) => ({
2208
+ messages: [
2209
+ {
2210
+ role: 'user',
2211
+ content: {
2212
+ type: 'text',
2213
+ text: [
2214
+ 'You are an expert Salesforce Marketing Cloud developer converting MCN Handlebars to AMPscript.',
2215
+ '',
2216
+ '## Instructions',
2217
+ '1. Call the `convertHandlebarsToAmpscript` **tool** with the code below to get a deterministic conversion.',
2218
+ '2. Review the `flaggedSections` — block helpers, partials, and binding paths often need context-specific AMPscript.',
2219
+ '3. For each flagged section, apply your expertise:',
2220
+ ' - `{{#each items}}…{{/each}}` → AMPscript `FOR @i = 1 TO RowCount(@items) DO … NEXT @i`',
2221
+ ' - `{{#if cond}}…{{else}}…{{/if}}` → `IF cond THEN … ELSE … ENDIF`',
2222
+ ' - `{!$…}` bindings and `mcn-platform` helpers may have no AMPscript equivalent — keep the MANUAL_REWRITE_REQUIRED note.',
2223
+ '4. Produce a single final AMPscript code block plus a short bulleted change log.',
2224
+ '',
2225
+ '## Handlebars code to convert',
2226
+ '```',
2227
+ code,
2228
+ '```',
2229
+ ].join('\n'),
2230
+ },
2231
+ },
2232
+ ],
2233
+ }));
2234
+ // ---------------------------------------------------------------------------
2235
+ // Tool: convertSsjsToHandlebars
2236
+ // ---------------------------------------------------------------------------
2237
+ server.tool('convertSsjsToHandlebars', 'Deterministically convert SSJS to Marketing Cloud Next (MCN) Handlebars transitively ' +
2238
+ '(SSJS → AMPscript → Handlebars). Because Handlebars is declarative, most imperative SSJS ' +
2239
+ 'has no Handlebars counterpart and is conservatively flagged MANUAL_REWRITE_REQUIRED; ' +
2240
+ 'inline Platform.Function.* calls that map through AMPscript to a Handlebars helper are ' +
2241
+ 'converted. Use the convertSsjsToHandlebars PROMPT for AI-enhanced handling of flagged sections.', {
2242
+ code: z
2243
+ .string()
2244
+ .describe('The SSJS code to convert (may include <script runat="server"> tags).'),
2245
+ }, ({ code }) => {
2246
+ const result = ssjsToHandlebars(code);
2247
+ return {
2248
+ content: [
2249
+ {
2250
+ type: 'text',
2251
+ text: JSON.stringify({
2252
+ convertedCode: result.convertedCode,
2253
+ changes: result.changes,
2254
+ flaggedSections: result.flaggedSections,
2255
+ }, null, 2),
2256
+ },
2257
+ ],
2258
+ };
2259
+ });
2260
+ // ---------------------------------------------------------------------------
2261
+ // Prompt: convertSsjsToHandlebars
2262
+ // ---------------------------------------------------------------------------
2263
+ server.prompt('convertSsjsToHandlebars', 'Convert SSJS to Marketing Cloud Next (MCN) Handlebars. Calls the convertSsjsToHandlebars ' +
2264
+ 'tool first (SSJS → AMPscript → Handlebars), then applies AI reasoning to handle the many ' +
2265
+ 'MANUAL_REWRITE_REQUIRED sections that imperative SSJS produces.', {
2266
+ code: z.string().describe('The SSJS code to convert to MCN Handlebars.'),
2267
+ }, ({ code }) => ({
2268
+ messages: [
2269
+ {
2270
+ role: 'user',
2271
+ content: {
2272
+ type: 'text',
2273
+ text: [
2274
+ 'You are an expert Salesforce Marketing Cloud developer converting SSJS to Marketing Cloud Next (MCN) Handlebars.',
2275
+ '',
2276
+ '## Key reality',
2277
+ '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).',
2278
+ '',
2279
+ '## Instructions',
2280
+ '1. Call the `convertSsjsToHandlebars` **tool** with the code below to get a deterministic conversion.',
2281
+ '2. Review the `flaggedSections` from BOTH stages ([SSJS→AMPscript] and [AMPscript→Handlebars]).',
2282
+ '3. For each flagged section, apply your expertise — but NEVER invent a Handlebars helper (use only the MCN catalog via `list_handlebars_helpers`).',
2283
+ '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.',
2284
+ '5. Validate your final output by calling `validate_handlebars`.',
2285
+ '6. Produce a single final Handlebars-in-HTML code block plus a short bulleted change log.',
2286
+ '',
2287
+ '## SSJS code to convert',
2288
+ '```javascript',
2289
+ code,
2290
+ '```',
2291
+ ].join('\n'),
2292
+ },
2293
+ },
2294
+ ],
2295
+ }));
2296
+ // ---------------------------------------------------------------------------
2297
+ // Tool: write_handlebars
2298
+ // ---------------------------------------------------------------------------
2299
+ server.tool('write_handlebars', 'Validate a Marketing Cloud Next (MCN) Handlebars-in-HTML draft so it can be finalized as ' +
2300
+ 'authored content. Runs the MCN Handlebars validator (locked-down engine) on the draft and ' +
2301
+ 'returns whether it is clean, plus any diagnostics (unknown/too-new helpers, unsupported ' +
2302
+ 'constructs, arity). Use the write_handlebars PROMPT to AUTHOR from intent — that prompt ' +
2303
+ 'instructs the model to use only catalog helpers and then call this tool to verify.', {
2304
+ draft: z.string().describe('The Handlebars-in-HTML draft to validate before finalizing.'),
2305
+ intent: z
2306
+ .string()
2307
+ .optional()
2308
+ .describe('Optional human description of what the content should do (echoed for context).'),
2309
+ }, ({ draft, intent }) => {
2310
+ const diagnostics = validateAmpscript(draft, {
2311
+ maxNumberOfProblems: 100,
2312
+ targetPlatform: 'next',
2313
+ }).filter((d) => d.source === 'handlebars');
2314
+ const isClean = diagnostics.length === 0;
2315
+ const header = isClean
2316
+ ? '✅ Draft is valid MCN Handlebars.'
2317
+ : `❌ Draft has ${diagnostics.length} MCN Handlebars issue(s) — fix before finalizing:`;
2318
+ const intentLine = intent ? `Intent: ${intent}\n\n` : '';
2319
+ return {
2320
+ content: [
2321
+ {
2322
+ type: 'text',
2323
+ text: `${intentLine}${header}\n\n${formatDiagnostics(diagnostics)}`,
2324
+ },
2325
+ ],
2326
+ };
2327
+ });
2328
+ // ---------------------------------------------------------------------------
2329
+ // Prompt: writeHandlebars
2330
+ // ---------------------------------------------------------------------------
2331
+ server.prompt('writeHandlebars', 'Author Marketing Cloud Next (MCN) Handlebars-in-HTML from a natural-language intent, using ' +
2332
+ 'only helpers that exist in the MCN catalog, then validate the result with write_handlebars.', {
2333
+ intent: z
2334
+ .string()
2335
+ .describe('What the content should do, e.g. "greet the subscriber by first name and show their loyalty tier".'),
2336
+ context: z
2337
+ .string()
2338
+ .optional()
2339
+ .describe('Optional data/personalization context (available fields, bindings, sample data).'),
2340
+ }, ({ intent, context }) => ({
2341
+ messages: [
2342
+ {
2343
+ role: 'user',
2344
+ content: {
2345
+ type: 'text',
2346
+ text: [
2347
+ 'You are an expert Salesforce Marketing Cloud developer authoring Marketing Cloud Next (MCN) Handlebars-in-HTML.',
2348
+ '',
2349
+ '## Hard rules',
2350
+ '- 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.',
2351
+ '- Respect each helper`s `mcnSince` availability.',
2352
+ '- Partials ({{> …}}), decorators ({{* …}}), and built-in helpers absent from MCN (e.g. {{log}}) are NOT allowed.',
2353
+ '- `{!$…}` bindings are merge fields, not helpers — use `list_handlebars_helpers` output / bindings catalog for valid ones.',
2354
+ '',
2355
+ '## Instructions',
2356
+ '1. Inspect the available catalog with `list_handlebars_helpers`.',
2357
+ '2. Author the Handlebars-in-HTML to satisfy the intent below.',
2358
+ '3. Call the `write_handlebars` **tool** with your draft to validate it.',
2359
+ '4. If validation reports issues, fix them and re-validate until clean.',
2360
+ '5. Return the final, validated Handlebars-in-HTML code block.',
2361
+ '',
2362
+ `## Intent`,
2363
+ intent,
2364
+ ...(context ? ['', '## Context', context] : []),
2365
+ ].join('\n'),
2366
+ },
2367
+ },
2368
+ ],
2369
+ }));
2370
+ // ---------------------------------------------------------------------------
1804
2371
  // Tool: get_server_version
1805
2372
  // ---------------------------------------------------------------------------
1806
2373
  server.tool('get_server_version', 'Return the running mcp-server-sfmc version and the size of the bundled Salesforce ' +