mcp-server-sfmc 2.0.0 → 2.1.1
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 +8 -4
- 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.map +1 -1
- package/dist/conversion-rules.js +255 -249
- package/dist/conversion-rules.js.map +1 -1
- package/dist/index.js +185 -152
- 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 -12
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
|
|
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,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 =
|
|
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
|
|
147
|
+
const parameters = helper.params
|
|
123
148
|
.map((p) => {
|
|
124
|
-
const
|
|
149
|
+
const request = p.optional ? '(optional)' : '(required)';
|
|
125
150
|
const variadic = p.variadic ? ' (variadic)' : '';
|
|
126
|
-
return ` - ${p.name}: ${p.type}${variadic} ${
|
|
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${
|
|
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
|
|
241
|
-
if (!
|
|
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
|
|
269
|
+
const parameters = function_.params
|
|
245
270
|
.map((p) => {
|
|
246
|
-
const
|
|
247
|
-
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 : ''}`;
|
|
248
273
|
})
|
|
249
274
|
.join('\n');
|
|
250
|
-
const examples =
|
|
275
|
+
const examples = function_.example ? '\n\nExample:\n' + function_.example : '';
|
|
251
276
|
// MCN compatibility badge
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const mcnLine =
|
|
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${
|
|
257
|
-
(
|
|
258
|
-
const text = `## ${
|
|
259
|
-
`**Category:** ${
|
|
260
|
-
`**Description:** ${
|
|
261
|
-
`**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)'}` +
|
|
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
|
|
532
|
-
if (!
|
|
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
|
|
562
|
+
const parameters = (function_.params ?? [])
|
|
538
563
|
.map((p) => {
|
|
539
564
|
const isOptional = p.optional || p.required === false;
|
|
540
|
-
const
|
|
541
|
-
return ` - ${p.name}: ${p.type ?? 'any'} ${
|
|
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 (
|
|
570
|
+
if (function_.deprecated) {
|
|
546
571
|
badges.push('⚠️ **Deprecated** — avoid in new code.');
|
|
547
572
|
}
|
|
548
|
-
if (
|
|
573
|
+
if (function_.requiresCoreLoad) {
|
|
549
574
|
badges.push('⚠️ **Requires** `Platform.Load("core", "1.1.5")` before calling this method.');
|
|
550
575
|
}
|
|
551
|
-
const header =
|
|
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 =
|
|
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:** ${
|
|
558
|
-
`**Parameters:**\n${
|
|
559
|
-
`**Returns:** ${
|
|
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
|
|
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
|
-
|
|
613
|
+
lineNumber++;
|
|
587
614
|
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
588
615
|
addedLines.push(line.slice(1));
|
|
589
|
-
lineMap.push(
|
|
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
|
|
603
|
-
const diagnostics = sfmcLanguageService.validate(
|
|
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 =
|
|
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
|
|
648
|
-
const diagnostics = sfmcLanguageService.validate(
|
|
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 =
|
|
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
|
|
711
|
-
if (
|
|
712
|
-
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]}")`;
|
|
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
|
|
734
|
-
let items = sfmcLanguageService.getCompletions(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
967
|
+
const lines = hits.map((h, index) => {
|
|
959
968
|
const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
|
|
960
|
-
return (`### ${
|
|
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((
|
|
975
|
-
const
|
|
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 `${
|
|
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((
|
|
997
|
-
const
|
|
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 `${
|
|
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
|
|
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} ${
|
|
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
|
|
1282
|
+
const developmentHits = searchMcnHelp(query, Math.ceil(limit / 2));
|
|
1274
1283
|
const opsHits = searchMceHelp(query, Math.floor(limit / 2), 'next');
|
|
1275
|
-
if (
|
|
1276
|
-
const parts =
|
|
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,
|
|
1289
|
+
const parts = opsHits.map((h, index) => {
|
|
1281
1290
|
const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
|
|
1282
|
-
return (`### ${
|
|
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,
|
|
1321
|
+
const parts = hits.map((h, index) => {
|
|
1313
1322
|
const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
|
|
1314
|
-
return (`### ${
|
|
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
|
-
'```' +
|
|
1527
|
+
'```' + fenceLang,
|
|
1502
1528
|
code,
|
|
1503
1529
|
'```',
|
|
1504
1530
|
'',
|
|
1505
1531
|
'## Review checklist',
|
|
1506
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
1840
|
-
const icon =
|
|
1868
|
+
for (const function_ of result.ampFunctions) {
|
|
1869
|
+
const icon = function_.status === 'supported'
|
|
1841
1870
|
? '✅'
|
|
1842
|
-
:
|
|
1871
|
+
: function_.status === 'needs-review'
|
|
1843
1872
|
? '⚠️'
|
|
1844
1873
|
: '❌';
|
|
1845
|
-
const statusLabel =
|
|
1874
|
+
const statusLabel = function_.status === 'supported'
|
|
1846
1875
|
? 'Supported'
|
|
1847
|
-
:
|
|
1876
|
+
: function_.status === 'needs-review'
|
|
1848
1877
|
? 'Needs review'
|
|
1849
1878
|
: 'Not supported';
|
|
1850
|
-
fileLines.push(`| ${
|
|
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 &&
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|