mcp-server-sfmc 2.0.0 → 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/dist/index.js CHANGED
@@ -21,7 +21,21 @@ import { ECMASCRIPT_BUILTINS, KNOWN_UNSUPPORTED, polyfillByPrototypeName, polyfi
21
21
  function projectPackageRoot() {
22
22
  return path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
23
23
  }
24
- const pkg = JSON.parse(fs.readFileSync(path.join(projectPackageRoot(), 'package.json'), 'utf8'));
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: pkg.version }, { instructions: SERVER_INSTRUCTIONS });
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,17 +138,17 @@ 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 = typeof d.message === 'string' ? d.message : d.message.value;
141
+ const message = diagnosticMessage(d.message);
117
142
  return `[${sev}] ${loc}: ${message}`;
118
143
  })
119
144
  .join('\n');
120
145
  }
121
146
  function formatHandlebarsHelper(helper) {
122
- const params = helper.params
147
+ const parameters = helper.params
123
148
  .map((p) => {
124
- const req = p.optional ? '(optional)' : '(required)';
149
+ const request = p.optional ? '(optional)' : '(required)';
125
150
  const variadic = p.variadic ? ' (variadic)' : '';
126
- return ` - ${p.name}: ${p.type}${variadic} ${req}${p.description ? ' — ' + p.description : ''}`;
151
+ return ` - ${p.name}: ${p.type}${variadic} ${request}${p.description ? ' — ' + p.description : ''}`;
127
152
  })
128
153
  .join('\n');
129
154
  const subexpr = helper.subexpressionOnly
@@ -135,7 +160,7 @@ function formatHandlebarsHelper(helper) {
135
160
  `**Type:** ${helper.helperType}\n\n` +
136
161
  `**Returns:** ${helper.returnType}\n\n` +
137
162
  `**Description:** ${helper.description}\n\n` +
138
- `**Parameters:**\n${params || ' (none)'}` +
163
+ `**Parameters:**\n${parameters || ' (none)'}` +
139
164
  subexpr +
140
165
  `\n\n✅ **Marketing Cloud Next:** Supported since API v${helper.mcnSince}.0` +
141
166
  (helper.docUrl ? `\n\n[Documentation](${helper.docUrl})` : ''));
@@ -237,28 +262,28 @@ server.tool('lookup_ampscript_function', 'Look up the signature, parameters, des
237
262
  'Case-insensitive. Returns null if the function is not found.', {
238
263
  name: z.string().describe('The AMPscript function name, e.g. "Lookup", "DateAdd", "IIf".'),
239
264
  }, ({ name }) => {
240
- const fn = sfmcLanguageService.lookupAmpscriptFunction(name);
241
- if (!fn) {
265
+ const function_ = sfmcLanguageService.lookupAmpscriptFunction(name);
266
+ if (!function_) {
242
267
  return { content: [{ type: 'text', text: `AMPscript function "${name}" not found.` }] };
243
268
  }
244
- const params = fn.params
269
+ const parameters = function_.params
245
270
  .map((p) => {
246
- const req = p.optional ? '(optional)' : '(required)';
247
- return ` - ${p.name}: ${p.type ?? 'any'} ${req}${p.description ? ' — ' + p.description : ''}`;
271
+ const request = p.optional ? '(optional)' : '(required)';
272
+ return ` - ${p.name}: ${p.type ?? 'any'} ${request}${p.description ? ' — ' + p.description : ''}`;
248
273
  })
249
274
  .join('\n');
250
- const examples = fn.example ? '\n\nExample:\n' + fn.example : '';
275
+ const examples = function_.example ? '\n\nExample:\n' + function_.example : '';
251
276
  // MCN compatibility badge
252
- const fnMcnSince = fn.mcnSince ?? null;
253
- const fnMcnNotes = fn.mcnNotes ?? null;
254
- const mcnLine = fnMcnSince === null
277
+ const functionMcnSince = function_.mcnSince ?? null;
278
+ const functionMcnNotes = function_.mcnNotes ?? null;
279
+ const mcnLine = functionMcnSince === null
255
280
  ? '\n\n❌ **Marketing Cloud Next:** Not supported'
256
- : `\n\n✅ **Marketing Cloud Next:** Supported since API v${fnMcnSince}.0` +
257
- (fnMcnNotes ? `\n> **MCN Note:** ${fnMcnNotes}` : '');
258
- const text = `## ${fn.name}\n\n` +
259
- `**Category:** ${fn.category ?? 'Unknown'}\n\n` +
260
- `**Description:** ${fn.description ?? ''}\n\n` +
261
- `**Parameters:**\n${params || ' (none)'}` +
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)'}` +
262
287
  examples +
263
288
  mcnLine;
264
289
  return { content: [{ type: 'text', text }] };
@@ -528,35 +553,37 @@ server.tool('lookup_ssjs_function', 'Authoritative lookup (sourced ONLY from the
528
553
  }, ({ name, owner }) => {
529
554
  // Strip namespace prefix for lookup
530
555
  const bare = name.replace(/^(Platform\.(Function|Variable|Response|Request|ClientBrowser|Recipient|DateTime)\.|WSProxy\.|HTTP\.|Script\.Util\.|Function\.|Variable\.|Response\.|Request\.)/i, '');
531
- const fn = sfmcLanguageService.lookupSsjsFunction(bare);
532
- if (!fn) {
556
+ const function_ = sfmcLanguageService.lookupSsjsFunction(bare);
557
+ if (!function_) {
533
558
  // Fall through to the ECMAScript built-in / polyfill / unsupported catalogs.
534
559
  const builtinResult = lookupSsjsBuiltin(name, owner);
535
560
  return { content: [{ type: 'text', text: builtinResult.text }] };
536
561
  }
537
- const params = (fn.params ?? [])
562
+ const parameters = (function_.params ?? [])
538
563
  .map((p) => {
539
564
  const isOptional = p.optional || p.required === false;
540
- const req = isOptional ? '(optional)' : '(required)';
541
- return ` - ${p.name}: ${p.type ?? 'any'} ${req}${p.description ? ' — ' + p.description : ''}`;
565
+ const request = isOptional ? '(optional)' : '(required)';
566
+ return ` - ${p.name}: ${p.type ?? 'any'} ${request}${p.description ? ' — ' + p.description : ''}`;
542
567
  })
543
568
  .join('\n');
544
569
  const badges = [];
545
- if (fn.deprecated) {
570
+ if (function_.deprecated) {
546
571
  badges.push('⚠️ **Deprecated** — avoid in new code.');
547
572
  }
548
- if (fn.requiresCoreLoad) {
573
+ if (function_.requiresCoreLoad) {
549
574
  badges.push('⚠️ **Requires** `Platform.Load("core", "1.1.5")` before calling this method.');
550
575
  }
551
- const header = fn.isStatic ? `## ${fn.name} *(static)*` : `## ${fn.name}`;
576
+ const header = function_.isStatic
577
+ ? `## ${function_.name} *(static)*`
578
+ : `## ${function_.name}`;
552
579
  const badgeBlock = badges.length > 0 ? badges.join('\n') + '\n\n' : '';
553
- const aliasLine = fn.aliasOf ? `**Alias of:** \`${fn.aliasOf}\`\n\n` : '';
580
+ const aliasLine = function_.aliasOf ? `**Alias of:** \`${function_.aliasOf}\`\n\n` : '';
554
581
  const text = `${header}\n\n` +
555
582
  badgeBlock +
556
583
  aliasLine +
557
- `**Description:** ${fn.description ?? ''}\n\n` +
558
- `**Parameters:**\n${params || ' (none)'}\n\n` +
559
- `**Returns:** ${fn.returnType ?? 'void'}`;
584
+ `**Description:** ${function_.description ?? ''}\n\n` +
585
+ `**Parameters:**\n${parameters || ' (none)'}\n\n` +
586
+ `**Returns:** ${function_.returnType ?? 'void'}`;
560
587
  return { content: [{ type: 'text', text }] };
561
588
  });
562
589
  // ---------------------------------------------------------------------------
@@ -580,13 +607,13 @@ server.tool('review_change', 'Review a code diff for SFMC (AMPscript, SSJS, or H
580
607
  }, ({ diff, language = 'auto', maxProblems = 50 }) => {
581
608
  // Extract added lines from the unified diff
582
609
  const addedLines = [];
583
- let lineNum = 0;
610
+ let lineNumber = 0;
584
611
  const lineMap = []; // maps index in addedLines to original diff line number
585
612
  for (const line of diff.split('\n')) {
586
- lineNum++;
613
+ lineNumber++;
587
614
  if (line.startsWith('+') && !line.startsWith('+++')) {
588
615
  addedLines.push(line.slice(1));
589
- lineMap.push(lineNum);
616
+ lineMap.push(lineNumber);
590
617
  }
591
618
  }
592
619
  if (addedLines.length === 0) {
@@ -599,8 +626,8 @@ server.tool('review_change', 'Review a code diff for SFMC (AMPscript, SSJS, or H
599
626
  ? detectLanguage(addedCode, 'html')
600
627
  : language;
601
628
  const settings = { maxNumberOfProblems: maxProblems };
602
- const doc = { text: addedCode, languageId: detectedLang, uri: 'diff' };
603
- const diagnostics = sfmcLanguageService.validate(doc, settings);
629
+ const document = { text: addedCode, languageId: detectedLang, uri: 'diff' };
630
+ const diagnostics = sfmcLanguageService.validate(document, settings);
604
631
  if (diagnostics.length === 0) {
605
632
  return {
606
633
  content: [
@@ -615,7 +642,7 @@ server.tool('review_change', 'Review a code diff for SFMC (AMPscript, SSJS, or H
615
642
  for (const d of diagnostics) {
616
643
  const sev = d.severity === 1 ? '🔴 ERROR' : d.severity === 2 ? '🟡 WARNING' : '🔵 INFO';
617
644
  const origLine = lineMap[d.range.start.line] ?? d.range.start.line + 1;
618
- const message = typeof d.message === 'string' ? d.message : d.message.value;
645
+ const message = diagnosticMessage(d.message);
619
646
  output.push(`${sev} (diff line ${origLine}): ${message}`);
620
647
  }
621
648
  return { content: [{ type: 'text', text: output.join('\n') }] };
@@ -644,15 +671,15 @@ server.tool('suggest_fix', 'Generate a corrected version of SFMC code based on v
644
671
  ? detectLanguage(code)
645
672
  : detectLanguage(code, language);
646
673
  const settings = { maxNumberOfProblems: 50, targetPlatform: target };
647
- const doc = { text: code, languageId: detectedLang, uri: 'fix-target' };
648
- const diagnostics = sfmcLanguageService.validate(doc, settings);
674
+ const document = { text: code, languageId: detectedLang, uri: 'fix-target' };
675
+ const diagnostics = sfmcLanguageService.validate(document, settings);
649
676
  const lines = code.split('\n');
650
677
  const suggestions = [];
651
678
  for (const d of diagnostics) {
652
679
  const lineText = lines[d.range.start.line] ?? '';
653
680
  // Diagnostic.message widened to `string | MarkupContent` in newer LSP types; the
654
681
  // SFMC language service only emits plain strings, so unwrap MarkupContent to its value.
655
- const message = typeof d.message === 'string' ? d.message : d.message.value;
682
+ const message = diagnosticMessage(d.message);
656
683
  suggestions.push(`Line ${d.range.start.line + 1}: ${message}\n` +
657
684
  ` Code: ${lineText.trim()}\n` +
658
685
  ` Fix: ${getFixSuggestion(message, lineText, detectedLang)}`);
@@ -707,9 +734,9 @@ function getFixSuggestion(message, line, lang) {
707
734
  if (m.includes('html comment'))
708
735
  return 'Remove the `<!-- -->` wrapper; use `/* comment */` inside AMPscript.';
709
736
  if (m.includes('unknown function')) {
710
- const fnMatch = message.match(/"([^"]+)"/);
711
- if (fnMatch)
712
- return `Check spelling — did you mean a known AMPscript function? ("${fnMatch[1]}")`;
737
+ const functionMatch = message.match(/"([^"]+)"/);
738
+ if (functionMatch)
739
+ return `Check spelling — did you mean a known AMPscript function? ("${functionMatch[1]}")`;
713
740
  }
714
741
  if (m.includes('expects'))
715
742
  return 'Check the number and types of arguments against the function signature.';
@@ -730,13 +757,11 @@ server.tool('get_ampscript_completions', 'Return a list of AMPscript function na
730
757
  .optional()
731
758
  .describe("Target platform. Use 'next' to return only completions supported in Marketing Cloud Next."),
732
759
  }, ({ code, line, character, target }) => {
733
- const doc = { text: code, languageId: 'ampscript', uri: 'completions' };
734
- let items = sfmcLanguageService.getCompletions(doc, { line, character });
760
+ const document = { text: code, languageId: 'ampscript', uri: 'completions' };
761
+ let items = sfmcLanguageService.getCompletions(document, { line, character });
735
762
  if (target === 'next') {
736
763
  items = items.filter((item) => {
737
- const label = typeof item.label === 'string'
738
- ? item.label
739
- : item.label.label;
764
+ const label = labelText(item);
740
765
  // Keep keywords and variables (non-function entries); filter out MCN-unsupported functions
741
766
  return !label.includes('(') || isMcnSupported(label.replace(/\(.*/, '').trim());
742
767
  });
@@ -744,9 +769,7 @@ server.tool('get_ampscript_completions', 'Return a list of AMPscript function na
744
769
  const formatted = items
745
770
  .slice(0, 50)
746
771
  .map((item) => {
747
- const label = typeof item.label === 'string'
748
- ? item.label
749
- : item.label.label;
772
+ const label = labelText(item);
750
773
  return `- ${label}${item.detail ? ` — ${item.detail}` : ''}`;
751
774
  })
752
775
  .join('\n');
@@ -789,19 +812,12 @@ server.tool('get_ssjs_completions', 'Return a list of SSJS Platform functions, W
789
812
  }
790
813
  const items = sfmcLanguageService.getSsjsCompletionCatalog();
791
814
  const filtered = filter
792
- ? items.filter((item) => {
793
- const label = typeof item.label === 'string'
794
- ? item.label
795
- : item.label.label;
796
- return label.toLowerCase().startsWith(filter.toLowerCase());
797
- })
815
+ ? items.filter((item) => labelText(item).toLowerCase().startsWith(filter.toLowerCase()))
798
816
  : items;
799
817
  const formatted = filtered
800
818
  .slice(0, 80)
801
819
  .map((item) => {
802
- const label = typeof item.label === 'string'
803
- ? item.label
804
- : item.label.label;
820
+ const label = labelText(item);
805
821
  return `- ${label}${item.detail ? ` — ${item.detail}` : ''}`;
806
822
  })
807
823
  .join('\n');
@@ -829,19 +845,12 @@ server.tool('get_handlebars_completions', 'Return Marketing Cloud Next (MCN) Han
829
845
  }, ({ filter }) => {
830
846
  const items = sfmcLanguageService.getHandlebarsCompletionCatalog();
831
847
  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
- })
848
+ ? items.filter((item) => labelText(item).toLowerCase().startsWith(filter.toLowerCase()))
838
849
  : items;
839
850
  const formatted = filtered
840
851
  .slice(0, 80)
841
852
  .map((item) => {
842
- const label = typeof item.label === 'string'
843
- ? item.label
844
- : item.label.label;
853
+ const label = labelText(item);
845
854
  return `- ${label}${item.detail ? ` — ${item.detail}` : ''}`;
846
855
  })
847
856
  .join('\n');
@@ -955,9 +964,9 @@ server.tool('search_mce_help', MCE_HELP_TOOL_DESC, {
955
964
  : `No matches for this query with product_focus="${focus}". Try broader keywords or product_focus="any".`;
956
965
  return { content: [{ type: 'text', text: hint }] };
957
966
  }
958
- const lines = hits.map((h, i) => {
967
+ const lines = hits.map((h, index) => {
959
968
  const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
960
- return (`### ${i + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
969
+ return (`### ${index + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
961
970
  `**Product:** ${h.chunk.productLabel}\n` +
962
971
  `**Score:** ${h.score}\n\n` +
963
972
  `${excerpt}${h.chunk.body.length > 520 ? '…' : ''}\n`);
@@ -971,11 +980,11 @@ server.tool('search_mce_help', MCE_HELP_TOOL_DESC, {
971
980
  // ---------------------------------------------------------------------------
972
981
  server.resource('ampscript-function-catalog', 'sfmc://ampscript/functions', async () => {
973
982
  const functions = sfmcLanguageService.getAllAmpscriptFunctions();
974
- const lines = functions.map((fn) => {
975
- const paramList = fn.params
983
+ const lines = functions.map((function_) => {
984
+ const parameterList = function_.params
976
985
  .map((p) => p.optional ? `[${p.name}: ${p.type ?? 'any'}]` : `${p.name}: ${p.type ?? 'any'}`)
977
986
  .join(', ');
978
- return `${fn.name}(${paramList}) — ${fn.description ?? ''}`;
987
+ return `${function_.name}(${parameterList}) — ${function_.description ?? ''}`;
979
988
  });
980
989
  return {
981
990
  contents: [
@@ -993,13 +1002,13 @@ server.resource('ampscript-function-catalog', 'sfmc://ampscript/functions', asyn
993
1002
  // ---------------------------------------------------------------------------
994
1003
  server.resource('ssjs-function-catalog', 'sfmc://ssjs/functions', async () => {
995
1004
  const functions = sfmcLanguageService.getAllSsjsFunctions();
996
- const lines = functions.map((fn) => {
997
- const paramList = (fn.params ?? [])
1005
+ const lines = functions.map((function_) => {
1006
+ const parameterList = (function_.params ?? [])
998
1007
  .map((p) => p.optional || p.required === false
999
1008
  ? `[${p.name}: ${p.type ?? 'any'}]`
1000
1009
  : `${p.name}: ${p.type ?? 'any'}`)
1001
1010
  .join(', ');
1002
- return `${fn.name}(${paramList}) — ${fn.description ?? ''}`;
1011
+ return `${function_.name}(${parameterList}) — ${function_.description ?? ''}`;
1003
1012
  });
1004
1013
  return {
1005
1014
  contents: [
@@ -1018,13 +1027,13 @@ server.resource('ssjs-function-catalog', 'sfmc://ssjs/functions', async () => {
1018
1027
  server.resource('handlebars-helper-catalog', 'sfmc://handlebars/helpers', async () => {
1019
1028
  const helpers = sfmcLanguageService.listHandlebarsHelpers();
1020
1029
  const lines = helpers.map((h) => {
1021
- const paramList = h.params
1030
+ const parameterList = h.params
1022
1031
  .map((p) => {
1023
1032
  const inner = `${p.name}: ${p.type}`;
1024
1033
  return p.optional ? `[${inner}]` : inner;
1025
1034
  })
1026
1035
  .join(', ');
1027
- return `{{${h.name} ${paramList}}} — (${h.category}, ${h.origin}, v${h.mcnSince}.0+) ${h.description}`;
1036
+ return `{{${h.name} ${parameterList}}} — (${h.category}, ${h.origin}, v${h.mcnSince}.0+) ${h.description}`;
1028
1037
  });
1029
1038
  return {
1030
1039
  contents: [
@@ -1140,7 +1149,7 @@ server.resource('mce-product-context', 'sfmc://mce/product-context', async () =>
1140
1149
  // ---------------------------------------------------------------------------
1141
1150
  server.resource('mce-help-index', 'sfmc://mce/help-index', async () => {
1142
1151
  const chunks = getChunks();
1143
- 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));
1144
1153
  const stats = getMceHelpStats();
1145
1154
  const scopeRows = Object.entries(stats.breakdown)
1146
1155
  .sort(([, a], [, b]) => b - a)
@@ -1159,7 +1168,7 @@ server.resource('mce-help-index', 'sfmc://mce/help-index', async () => {
1159
1168
  // ---------------------------------------------------------------------------
1160
1169
  server.resource('mcn-help-index', 'sfmc://mcn/help-index', async () => {
1161
1170
  const chunks = getMcnChunks();
1162
- 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));
1163
1172
  const stats = getMcnHelpStats();
1164
1173
  const text = `# Bundled Marketing Cloud Next developer API docs (${stats.chunkCount} sections from ${stats.fileCount} files)\n\n` +
1165
1174
  `## Files\n\n` +
@@ -1270,16 +1279,16 @@ server.tool('search_help', 'Unified help search that automatically detects the t
1270
1279
  const sections = [];
1271
1280
  if (platform === 'next') {
1272
1281
  // MCN: search both the developer API reference and the MCN operational/admin help
1273
- const devHits = searchMcnHelp(query, Math.ceil(limit / 2));
1282
+ const developmentHits = searchMcnHelp(query, Math.ceil(limit / 2));
1274
1283
  const opsHits = searchMceHelp(query, Math.floor(limit / 2), 'next');
1275
- if (devHits.length > 0) {
1276
- const parts = devHits.map(({ chunk }) => `### ${chunk.heading}\n*Source: ${chunk.relativePath}*\n\n${chunk.body}`);
1284
+ if (developmentHits.length > 0) {
1285
+ const parts = developmentHits.map(({ chunk }) => `### ${chunk.heading}\n*Source: ${chunk.relativePath}*\n\n${chunk.body}`);
1277
1286
  sections.push(`## MCN Developer API Docs\n\n${parts.join('\n\n---\n\n')}`);
1278
1287
  }
1279
1288
  if (opsHits.length > 0) {
1280
- const parts = opsHits.map((h, i) => {
1289
+ const parts = opsHits.map((h, index) => {
1281
1290
  const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
1282
- return (`### ${i + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
1291
+ return (`### ${index + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
1283
1292
  `**Score:** ${h.score}\n\n` +
1284
1293
  `${excerpt}${h.chunk.body.length > 520 ? '…' : ''}`);
1285
1294
  });
@@ -1309,9 +1318,9 @@ server.tool('search_help', 'Unified help search that automatically detects the t
1309
1318
  : `No results found for "${query}". Try broader keywords.`;
1310
1319
  return { content: [{ type: 'text', text: hint }] };
1311
1320
  }
1312
- const parts = hits.map((h, i) => {
1321
+ const parts = hits.map((h, index) => {
1313
1322
  const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
1314
- return (`### ${i + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
1323
+ return (`### ${index + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
1315
1324
  `**Product:** ${h.chunk.productLabel}\n` +
1316
1325
  `**Score:** ${h.score}\n\n` +
1317
1326
  `${excerpt}${h.chunk.body.length > 520 ? '…' : ''}`);
@@ -1482,6 +1491,23 @@ server.prompt('reviewSfmcCode', 'Review SFMC code for correctness, best practice
1482
1491
  const platformNote = target === 'next'
1483
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).'
1484
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;
1485
1511
  return {
1486
1512
  messages: [
1487
1513
  {
@@ -1498,26 +1524,12 @@ server.prompt('reviewSfmcCode', 'Review SFMC code for correctness, best practice
1498
1524
  focus ? `Focus especially on: ${focus}` : '',
1499
1525
  '',
1500
1526
  '## Code to Review',
1501
- '```' + (detectedLang === 'ssjs' ? 'javascript' : detectedLang),
1527
+ '```' + fenceLang,
1502
1528
  code,
1503
1529
  '```',
1504
1530
  '',
1505
1531
  '## Review checklist',
1506
- detectedLang === 'ampscript'
1507
- ? [
1508
- '- Delimiter balance (%%[ ]%%, %%= =%%)',
1509
- '- IF/ENDIF, FOR/NEXT block balance',
1510
- '- Correct function names and argument counts',
1511
- '- Correct comment syntax (/* */ only)',
1512
- '- Proper variable declaration with @',
1513
- ].join('\n') + platformNote
1514
- : [
1515
- '- No ES6+ syntax (var, not let/const; no arrow functions)',
1516
- '- Platform.Load before Core library objects',
1517
- '- Correct Platform.Function calls',
1518
- '- WSProxy error handling',
1519
- '- No sensitive data in logs or responses',
1520
- ].join('\n') + platformNote,
1532
+ checklist,
1521
1533
  ]
1522
1534
  .filter(Boolean)
1523
1535
  .join('\n'),
@@ -1633,16 +1645,56 @@ server.prompt('answerMceHowTo', 'Answer a Marketing Cloud **administration or se
1633
1645
  // ---------------------------------------------------------------------------
1634
1646
  // Tool: check_mcn_compatibility
1635
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
+ }
1636
1693
  server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files for Marketing Cloud Next (MCN) readiness. ' +
1637
1694
  'Returns an executive summary and a per-file, per-function report with migration difficulty. ' +
1638
1695
  'SSJS blocks that only use Platform.Function.* calls are classified as "Needs conversion" (not "Not migratable"). ' +
1639
1696
  'Use this tool before using rewrite_for_mcn.', {
1640
- files: z
1641
- .array(z.object({
1642
- filename: z.string().describe('File name (e.g. "email-template.html").'),
1643
- content: z.string().describe('Full file content to analyze.'),
1644
- }))
1645
- .describe('List of files to analyze.'),
1697
+ files: mcnFileArraySchema,
1646
1698
  }, ({ files }) => {
1647
1699
  const results = [];
1648
1700
  for (const { filename, content } of files) {
@@ -1690,14 +1742,7 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
1690
1742
  const blockCode = blockMatch[1];
1691
1743
  const lineApprox = content.slice(0, blockMatch.index).split('\n').length;
1692
1744
  // Check for non-migratable patterns
1693
- let notMigratableReason = '';
1694
- for (const { pattern, reason } of NON_MIGRATABLE_SSJS_PATTERNS) {
1695
- pattern.lastIndex = 0;
1696
- if (pattern.test(blockCode)) {
1697
- notMigratableReason = reason;
1698
- break;
1699
- }
1700
- }
1745
+ const notMigratableReason = firstNonMigratableSsjsReason(blockCode);
1701
1746
  if (notMigratableReason) {
1702
1747
  ssjsBlocks.push({
1703
1748
  index: blockIndex,
@@ -1727,7 +1772,7 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
1727
1772
  }).filter((d) => d.source === 'handlebars');
1728
1773
  for (const d of hbsDiagnostics) {
1729
1774
  const severity = d.severity === 1 ? 'error' : d.severity === 2 ? 'warning' : 'info';
1730
- const message = typeof d.message === 'string' ? d.message : d.message.value;
1775
+ const message = diagnosticMessage(d.message);
1731
1776
  handlebarsProblems.push({
1732
1777
  line: d.range.start.line + 1,
1733
1778
  severity,
@@ -1737,26 +1782,10 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
1737
1782
  // Report recognized helper usages with their MCN availability version.
1738
1783
  // Helper names are matched against handlebars-data via the LSP lookup so
1739
1784
  // 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
- }
1785
+ handlebarsHelpers.push(...collectHandlebarsHelperUsages(content));
1757
1786
  }
1758
1787
  // 3. Assess per-file difficulty
1759
- const hasCloudPagesFn = ampFunctions.some((f) => CLOUDPAGES_ONLY_FUNCTIONS.has(f.name.toLowerCase()) &&
1788
+ const hasCloudPagesFunction = ampFunctions.some((f) => CLOUDPAGES_ONLY_FUNCTIONS.has(f.name.toLowerCase()) &&
1760
1789
  f.status === 'not-supported');
1761
1790
  const hasNotMigratableSsjs = ssjsBlocks.some((b) => b.status === 'not-migratable');
1762
1791
  const hasUnsupportedAmp = ampFunctions.some((f) => f.status === 'not-supported' &&
@@ -1766,7 +1795,7 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
1766
1795
  const hasHandlebarsError = handlebarsProblems.some((p) => p.severity === 'error');
1767
1796
  const hasHandlebarsWarning = handlebarsProblems.some((p) => p.severity === 'warning');
1768
1797
  let difficulty;
1769
- if (hasCloudPagesFn || hasNotMigratableSsjs) {
1798
+ if (hasCloudPagesFunction || hasNotMigratableSsjs) {
1770
1799
  difficulty = 'not-migratable';
1771
1800
  }
1772
1801
  else if (hasUnsupportedAmp || hasConvertibleSsjs || hasHandlebarsError) {
@@ -1836,18 +1865,18 @@ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files
1836
1865
  }
1837
1866
  if (result.ampFunctions.length > 0) {
1838
1867
  fileLines.push('| Function | Line | Status | Reason |', '|---|---|---|---|');
1839
- for (const fn of result.ampFunctions) {
1840
- const icon = fn.status === 'supported'
1868
+ for (const function_ of result.ampFunctions) {
1869
+ const icon = function_.status === 'supported'
1841
1870
  ? '✅'
1842
- : fn.status === 'needs-review'
1871
+ : function_.status === 'needs-review'
1843
1872
  ? '⚠️'
1844
1873
  : '❌';
1845
- const statusLabel = fn.status === 'supported'
1874
+ const statusLabel = function_.status === 'supported'
1846
1875
  ? 'Supported'
1847
- : fn.status === 'needs-review'
1876
+ : function_.status === 'needs-review'
1848
1877
  ? 'Needs review'
1849
1878
  : 'Not supported';
1850
- fileLines.push(`| ${fn.name} | ${fn.line} | ${icon} ${statusLabel} | ${fn.reason} |`);
1879
+ fileLines.push(`| ${function_.name} | ${function_.line} | ${icon} ${statusLabel} | ${function_.reason} |`);
1851
1880
  }
1852
1881
  }
1853
1882
  else if (result.ssjsBlocks.length === 0 &&
@@ -1951,7 +1980,8 @@ server.tool('rewrite_for_mcn', 'Deterministically rewrite AMPscript (and optiona
1951
1980
  });
1952
1981
  // Reassess difficulty
1953
1982
  const hasMigratable = allNonMigratable.length > 0;
1954
- const difficulty = hasMigratable && allNonMigratable.some((i) => i.reason.includes('not-migratable'))
1983
+ const difficulty = hasMigratable &&
1984
+ allNonMigratable.some((index) => index.reason.includes('not-migratable'))
1955
1985
  ? 'not-migratable'
1956
1986
  : ampResult.difficulty;
1957
1987
  const result = {
@@ -2382,7 +2412,7 @@ server.tool('get_server_version', 'Return the running mcp-server-sfmc version an
2382
2412
  type: 'text',
2383
2413
  text: JSON.stringify({
2384
2414
  name: 'mcp-server-sfmc',
2385
- version: pkg.version,
2415
+ version: package_.version,
2386
2416
  mceHelp: { chunkCount: mce.chunkCount, fileCount: mceFileCount },
2387
2417
  mcnHelp: { chunkCount: mcn.chunkCount, fileCount: mcn.fileCount },
2388
2418
  }, null, 2),
@@ -2395,15 +2425,18 @@ server.tool('get_server_version', 'Return the running mcp-server-sfmc version an
2395
2425
  // ---------------------------------------------------------------------------
2396
2426
  async function main() {
2397
2427
  if (process.argv.includes('--version') || process.argv.includes('-v')) {
2398
- process.stdout.write(pkg.version + '\n');
2428
+ process.stdout.write(package_.version + '\n');
2399
2429
  return;
2400
2430
  }
2401
2431
  const transport = new StdioServerTransport();
2402
2432
  await server.connect(transport);
2403
2433
  process.stderr.write('mcp-server-sfmc running on stdio\n');
2404
2434
  }
2405
- main().catch((ex) => {
2435
+ try {
2436
+ await main();
2437
+ }
2438
+ catch (ex) {
2406
2439
  process.stderr.write(`Fatal: ${String(ex)}\n`);
2407
2440
  process.exit(1);
2408
- });
2441
+ }
2409
2442
  //# sourceMappingURL=index.js.map