mcp-server-sfmc 0.4.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13,8 +13,10 @@ import { fileURLToPath } from 'node:url';
13
13
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
14
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
15
  import { z } from 'zod';
16
- import { sfmcLanguageService, validateAmpscript, validateSsjs, validateGtlBlocks, } from 'sfmc-language-lsp';
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
+ 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';
18
20
  function projectPackageRoot() {
19
21
  return path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
20
22
  }
@@ -23,6 +25,17 @@ const pkg = JSON.parse(fs.readFileSync(path.join(projectPackageRoot(), 'package.
23
25
  // Server instance
24
26
  // ---------------------------------------------------------------------------
25
27
  const SERVER_INSTRUCTIONS = 'This server provides authoritative SFMC language intelligence and bundled Salesforce Marketing Cloud product help.\n\n' +
28
+ '## Target platform — detect before writing or validating code\n\n' +
29
+ 'Before writing, validating, converting, or completing any AMPscript or SSJS code:\n' +
30
+ '1. Call `detect_sfmc_platform` with the project root path.\n' +
31
+ ' - Returns `"engagement"` (`.mcdevrc.json` found) → use `target: "engagement"`\n' +
32
+ ' - Returns `"next"` (`sfdx-project.json` found) → use `target: "next"`\n' +
33
+ ' - Returns `"unknown"` → ask the user: "Are you targeting Marketing Cloud Engagement (MCE) or Marketing Cloud Next (MCN)?"\n' +
34
+ '2. Pass the resolved target to all language tools (`validate_*`, `suggest_fix`, `get_*_completions`).\n' +
35
+ '3. When target is `"next"`: SSJS is **not supported** — do not generate SSJS. Only use AMPscript functions that are MCN-supported (check with `list_ampscript_functions` with `platform: "next"`).\n\n' +
36
+ '## When to call search_mcn_help\n\n' +
37
+ '**ALWAYS** call `search_mcn_help` before answering questions about Marketing Cloud Next **developer APIs**, ' +
38
+ 'objects, flows, segments, transactional messages, REST/SOAP APIs, or AMPscript behavior differences in MCN.\n\n' +
26
39
  '## When to call search_mce_help\n\n' +
27
40
  '**ALWAYS** call `search_mce_help` before answering any question about the following product areas ' +
28
41
  '(use the matching `product_focus` value for best results):\n\n' +
@@ -35,7 +48,7 @@ const SERVER_INSTRUCTIONS = 'This server provides authoritative SFMC language in
35
48
  '| Subscriptions, sending limits, suspended accounts, Einstein features | `engagement` |\n' +
36
49
  '| Advertising, Distributed Marketing, Marketing Cloud Connect | `engagement` |\n' +
37
50
  '| Contact Builder, Audience Builder, Data Extensions | `engagement` |\n' +
38
- '| Marketing Cloud Next, migration to Next | `next` |\n' +
51
+ '| Marketing Cloud Next migration, admin, setup, or operational overview | `next` |\n' +
39
52
  '| Marketing Cloud Personalization / Interaction Studio, real-time personalisation, A/B testing | `personalization` |\n' +
40
53
  '| Salesforce Personalization | `personalization` |\n' +
41
54
  '| Marketing Cloud Account Engagement / Pardot, B2B marketing automation, lead scoring | `account-engagement` |\n' +
@@ -45,7 +58,13 @@ const SERVER_INSTRUCTIONS = 'This server provides authoritative SFMC language in
45
58
  'in the answer, and note when excerpts are incomplete.\n\n' +
46
59
  '## When to call language tools\n\n' +
47
60
  'For AMPscript/SSJS/GTL code tasks use `validate_*`, `lookup_*`, `review_change`, `suggest_fix`, etc. ' +
48
- 'Do **not** guess function signatures — call `lookup_ampscript_function` or `lookup_ssjs_function`.';
61
+ 'Do **not** guess function signatures — call `lookup_ampscript_function` or `lookup_ssjs_function`.\n\n' +
62
+ '## Unified help search shortcut\n\n' +
63
+ 'When you already know the project root, prefer `search_help` over calling `search_mce_help` or ' +
64
+ '`search_mcn_help` directly. Pass `projectRoot` and `search_help` will auto-detect the platform ' +
65
+ 'and route the query to the right doc index (or both for MCN). Only fall back to the individual ' +
66
+ 'search tools when you need to scope by `product_focus` (MCE only) or when you are **certain** ' +
67
+ 'which doc index to target.';
49
68
  const server = new McpServer({ name: 'mcp-server-sfmc', version: pkg.version }, { instructions: SERVER_INSTRUCTIONS });
50
69
  function detectLanguage(code, hint) {
51
70
  if (hint === 'ssjs')
@@ -83,7 +102,8 @@ function formatDiagnostics(diagnostics) {
83
102
  // Tool: validate_ampscript
84
103
  // ---------------------------------------------------------------------------
85
104
  server.tool('validate_ampscript', 'Validate AMPscript code for syntax errors, unknown functions, arity mismatches, and style issues. ' +
86
- 'Returns a list of diagnostics with line numbers and severity.', {
105
+ 'Returns a list of diagnostics with line numbers and severity. ' +
106
+ "Set target to 'next' to also report functions not supported in Marketing Cloud Next.", {
87
107
  code: z.string().describe('The AMPscript code to validate. May include HTML context.'),
88
108
  maxProblems: z
89
109
  .number()
@@ -92,8 +112,15 @@ server.tool('validate_ampscript', 'Validate AMPscript code for syntax errors, un
92
112
  .max(500)
93
113
  .optional()
94
114
  .describe('Maximum number of problems to return (default 100).'),
95
- }, ({ code, maxProblems }) => {
96
- const settings = { maxNumberOfProblems: maxProblems ?? 100 };
115
+ target: z
116
+ .enum(['engagement', 'next'])
117
+ .optional()
118
+ .describe("Target platform. Use 'next' to flag AMPscript functions not supported in Marketing Cloud Next."),
119
+ }, ({ code, maxProblems, target }) => {
120
+ const settings = {
121
+ maxNumberOfProblems: maxProblems ?? 100,
122
+ targetPlatform: target,
123
+ };
97
124
  const diagnostics = validateAmpscript(code, settings);
98
125
  return {
99
126
  content: [{ type: 'text', text: formatDiagnostics(diagnostics) }],
@@ -103,7 +130,8 @@ server.tool('validate_ampscript', 'Validate AMPscript code for syntax errors, un
103
130
  // Tool: validate_ssjs
104
131
  // ---------------------------------------------------------------------------
105
132
  server.tool('validate_ssjs', 'Validate SSJS (Server-Side JavaScript) code for unsupported ES6+ syntax, missing Platform.Load, ' +
106
- 'and incorrect usage patterns. Returns diagnostics with line numbers.', {
133
+ 'and incorrect usage patterns. Returns diagnostics with line numbers. ' +
134
+ "Set target to 'next' to flag all SSJS as unsupported (SSJS is not available in Marketing Cloud Next).", {
107
135
  code: z
108
136
  .string()
109
137
  .describe('The SSJS code to validate. May include <script runat="server"> tags.'),
@@ -114,8 +142,15 @@ server.tool('validate_ssjs', 'Validate SSJS (Server-Side JavaScript) code for un
114
142
  .max(500)
115
143
  .optional()
116
144
  .describe('Maximum number of problems to return (default 100).'),
117
- }, ({ code, maxProblems }) => {
118
- const settings = { maxNumberOfProblems: maxProblems ?? 100 };
145
+ target: z
146
+ .enum(['engagement', 'next'])
147
+ .optional()
148
+ .describe("Target platform. Use 'next' to flag SSJS code as unsupported in Marketing Cloud Next."),
149
+ }, ({ code, maxProblems, target }) => {
150
+ const settings = {
151
+ maxNumberOfProblems: maxProblems ?? 100,
152
+ targetPlatform: target,
153
+ };
119
154
  const diagnostics = validateSsjs(code, settings);
120
155
  return {
121
156
  content: [{ type: 'text', text: formatDiagnostics(diagnostics) }],
@@ -125,7 +160,8 @@ server.tool('validate_ssjs', 'Validate SSJS (Server-Side JavaScript) code for un
125
160
  // Tool: validate_sfmc_html
126
161
  // ---------------------------------------------------------------------------
127
162
  server.tool('validate_sfmc_html', 'Validate an HTML file that contains embedded AMPscript and/or SSJS blocks. ' +
128
- 'Checks both languages and GTL template syntax.', {
163
+ 'Checks both languages and GTL template syntax. ' +
164
+ "Set target to 'next' to flag MCN-unsupported AMPscript functions and all SSJS as errors.", {
129
165
  code: z
130
166
  .string()
131
167
  .describe('HTML source that may contain %%[ ]%%, %%= =%%, <script runat="server">, or {{ }} blocks.'),
@@ -136,9 +172,13 @@ server.tool('validate_sfmc_html', 'Validate an HTML file that contains embedded
136
172
  .max(500)
137
173
  .optional()
138
174
  .describe('Maximum number of problems to return (default 100).'),
139
- }, ({ code, maxProblems }) => {
175
+ target: z
176
+ .enum(['engagement', 'next'])
177
+ .optional()
178
+ .describe("Target platform. Use 'next' to flag MCN-incompatible code (unsupported AMPscript functions and all SSJS)."),
179
+ }, ({ code, maxProblems, target }) => {
140
180
  const limit = maxProblems ?? 100;
141
- const settings = { maxNumberOfProblems: limit };
181
+ const settings = { maxNumberOfProblems: limit, targetPlatform: target };
142
182
  const ampDiags = validateAmpscript(code, settings);
143
183
  const ssjsDiags = validateSsjs(code, settings);
144
184
  const gtlDiags = [];
@@ -166,14 +206,69 @@ server.tool('lookup_ampscript_function', 'Look up the signature, parameters, des
166
206
  })
167
207
  .join('\n');
168
208
  const examples = fn.example ? '\n\nExample:\n' + fn.example : '';
209
+ // MCN compatibility badge
210
+ const fnMcnSince = fn.mcnSince ?? null;
211
+ const fnMcnNotes = fn.mcnNotes ?? null;
212
+ const mcnLine = fnMcnSince === null
213
+ ? '\n\n❌ **Marketing Cloud Next:** Not supported'
214
+ : `\n\n✅ **Marketing Cloud Next:** Supported since API v${fnMcnSince}.0` +
215
+ (fnMcnNotes ? `\n> **MCN Note:** ${fnMcnNotes}` : '');
169
216
  const text = `## ${fn.name}\n\n` +
170
217
  `**Category:** ${fn.category ?? 'Unknown'}\n\n` +
171
218
  `**Description:** ${fn.description ?? ''}\n\n` +
172
219
  `**Parameters:**\n${params || ' (none)'}` +
173
- examples;
220
+ examples +
221
+ mcnLine;
174
222
  return { content: [{ type: 'text', text }] };
175
223
  });
176
224
  // ---------------------------------------------------------------------------
225
+ // Tool: list_ampscript_functions
226
+ // ---------------------------------------------------------------------------
227
+ server.tool('list_ampscript_functions', 'List all AMPscript functions, optionally filtered by category and/or target platform. ' +
228
+ "Use platform: 'next' to return only functions supported in Marketing Cloud Next.", {
229
+ category: z
230
+ .string()
231
+ .optional()
232
+ .describe('Filter by function category (case-insensitive substring match), e.g. "data extension", "string", "date".'),
233
+ platform: z
234
+ .enum(['engagement', 'next'])
235
+ .optional()
236
+ .describe("Filter by target platform. Use 'next' to show only functions available in Marketing Cloud Next (API v67.0+)."),
237
+ }, ({ category, platform }) => {
238
+ const all = sfmcLanguageService.listAmpscriptFunctions();
239
+ let filtered = all;
240
+ if (platform === 'next') {
241
+ filtered = filtered.filter((f) => isMcnSupported(f.name));
242
+ }
243
+ if (category) {
244
+ const catLower = category.toLowerCase();
245
+ filtered = filtered.filter((f) => f.category?.toLowerCase().includes(catLower));
246
+ }
247
+ if (filtered.length === 0) {
248
+ return {
249
+ content: [{ type: 'text', text: 'No AMPscript functions match the filter.' }],
250
+ };
251
+ }
252
+ const platformHeader = platform === 'next'
253
+ ? '*(Marketing Cloud Next — API v67.0+ supported only)*'
254
+ : '*(Marketing Cloud Engagement — all functions)*';
255
+ const rows = filtered
256
+ .map((f) => {
257
+ const mcnTag = getMcnApiVersion(f.name) === null ? '' : ' ✅';
258
+ return `- **${f.name}**${mcnTag} *(${f.category ?? 'Unknown'})*: ${f.description ?? ''}`;
259
+ })
260
+ .join('\n');
261
+ const legend = platform === 'next' ? '' : '\n\n*Legend: ✅ = supported in Marketing Cloud Next*';
262
+ return {
263
+ content: [
264
+ {
265
+ type: 'text',
266
+ text: `## AMPscript Functions ${platformHeader}\n\n${rows}${legend}`,
267
+ },
268
+ ],
269
+ };
270
+ });
271
+ // ---------------------------------------------------------------------------
177
272
  // Tool: lookup_ssjs_function
178
273
  // ---------------------------------------------------------------------------
179
274
  server.tool('lookup_ssjs_function', 'Look up the signature, parameters, and description for an SSJS function or method. ' +
@@ -280,7 +375,8 @@ server.tool('review_change', 'Review a code diff for SFMC (AMPscript, SSJS, or H
280
375
  // Tool: suggest_fix
281
376
  // ---------------------------------------------------------------------------
282
377
  server.tool('suggest_fix', 'Generate a corrected version of SFMC code based on validation diagnostics. ' +
283
- 'Returns the original code with inline fix suggestions or a corrected replacement.', {
378
+ 'Returns the original code with inline fix suggestions or a corrected replacement. ' +
379
+ "Set target to 'next' to include MCN platform compatibility in the analysis.", {
284
380
  code: z.string().describe('The SFMC code snippet to fix.'),
285
381
  language: z
286
382
  .enum(['ampscript', 'ssjs', 'html', 'auto'])
@@ -290,11 +386,15 @@ server.tool('suggest_fix', 'Generate a corrected version of SFMC code based on v
290
386
  .string()
291
387
  .optional()
292
388
  .describe('Optional human description of the specific issue to fix.'),
293
- }, ({ code, language = 'auto', issueDescription }) => {
389
+ target: z
390
+ .enum(['engagement', 'next'])
391
+ .optional()
392
+ .describe("Target platform. Use 'next' to flag MCN-incompatible functions and SSJS usage."),
393
+ }, ({ code, language = 'auto', issueDescription, target }) => {
294
394
  const detectedLang = language === 'auto'
295
395
  ? detectLanguage(code)
296
396
  : detectLanguage(code, language);
297
- const settings = { maxNumberOfProblems: 50 };
397
+ const settings = { maxNumberOfProblems: 50, targetPlatform: target };
298
398
  const doc = { text: code, languageId: detectedLang, uri: 'fix-target' };
299
399
  const diagnostics = sfmcLanguageService.validate(doc, settings);
300
400
  const lines = code.split('\n');
@@ -350,13 +450,27 @@ function getFixSuggestion(message, line, lang) {
350
450
  // ---------------------------------------------------------------------------
351
451
  // Tool: get_ampscript_completions
352
452
  // ---------------------------------------------------------------------------
353
- server.tool('get_ampscript_completions', 'Return a list of AMPscript function names, keywords, and variable names available at a given position in the code.', {
453
+ server.tool('get_ampscript_completions', 'Return a list of AMPscript function names, keywords, and variable names available at a given position in the code. ' +
454
+ "Set target to 'next' to filter completions to only MCN-supported functions.", {
354
455
  code: z.string().describe('The full AMPscript document text.'),
355
456
  line: z.number().int().min(0).describe('Zero-based line number of the cursor position.'),
356
457
  character: z.number().int().min(0).describe('Zero-based character offset within the line.'),
357
- }, ({ code, line, character }) => {
458
+ target: z
459
+ .enum(['engagement', 'next'])
460
+ .optional()
461
+ .describe("Target platform. Use 'next' to return only completions supported in Marketing Cloud Next."),
462
+ }, ({ code, line, character, target }) => {
358
463
  const doc = { text: code, languageId: 'ampscript', uri: 'completions' };
359
- const items = sfmcLanguageService.getCompletions(doc, { line, character });
464
+ let items = sfmcLanguageService.getCompletions(doc, { line, character });
465
+ if (target === 'next') {
466
+ items = items.filter((item) => {
467
+ const label = typeof item.label === 'string'
468
+ ? item.label
469
+ : item.label.label;
470
+ // Keep keywords and variables (non-function entries); filter out MCN-unsupported functions
471
+ return !label.includes('(') || isMcnSupported(label.replace(/\(.*/, '').trim());
472
+ });
473
+ }
360
474
  const formatted = items
361
475
  .slice(0, 50)
362
476
  .map((item) => {
@@ -367,13 +481,14 @@ server.tool('get_ampscript_completions', 'Return a list of AMPscript function na
367
481
  })
368
482
  .join('\n');
369
483
  const total = items.length;
484
+ const platformNote = target === 'next' ? ' (Marketing Cloud Next — MCN-supported only)' : '';
370
485
  return {
371
486
  content: [
372
487
  {
373
488
  type: 'text',
374
489
  text: total === 0
375
490
  ? 'No completions at this position (cursor is outside an AMPscript block).'
376
- : `${total} completions available (showing up to 50):\n\n${formatted}`,
491
+ : `${total} completions available${platformNote} (showing up to 50):\n\n${formatted}`,
377
492
  },
378
493
  ],
379
494
  };
@@ -381,12 +496,27 @@ server.tool('get_ampscript_completions', 'Return a list of AMPscript function na
381
496
  // ---------------------------------------------------------------------------
382
497
  // Tool: get_ssjs_completions
383
498
  // ---------------------------------------------------------------------------
384
- server.tool('get_ssjs_completions', 'Return a list of SSJS Platform functions, WSProxy methods, and other SFMC-specific identifiers available for completion.', {
499
+ server.tool('get_ssjs_completions', 'Return a list of SSJS Platform functions, WSProxy methods, and other SFMC-specific identifiers available for completion. ' +
500
+ "When target is 'next', returns an empty list with a note — SSJS is not supported in Marketing Cloud Next.", {
385
501
  filter: z
386
502
  .string()
387
503
  .optional()
388
504
  .describe('Optional prefix filter, e.g. "Platform.Function" or "WSProxy".'),
389
- }, ({ filter }) => {
505
+ target: z
506
+ .enum(['engagement', 'next'])
507
+ .optional()
508
+ .describe("Target platform. Use 'next' to indicate MCN context — SSJS is not available in MCN."),
509
+ }, ({ filter, target }) => {
510
+ if (target === 'next') {
511
+ return {
512
+ content: [
513
+ {
514
+ type: 'text',
515
+ text: 'SSJS is not supported in Marketing Cloud Next (MCN). Use AMPscript instead. Call `get_ampscript_completions` with `target: "next"` for MCN-compatible function completions.',
516
+ },
517
+ ],
518
+ };
519
+ }
390
520
  const items = sfmcLanguageService.getSsjsCompletionCatalog();
391
521
  const filtered = filter
392
522
  ? items.filter((item) => {
@@ -641,16 +771,198 @@ server.resource('mce-help-index', 'sfmc://mce/help-index', async () => {
641
771
  };
642
772
  });
643
773
  // ---------------------------------------------------------------------------
774
+ // Resource: mcn-help index (bundled files)
775
+ // ---------------------------------------------------------------------------
776
+ server.resource('mcn-help-index', 'sfmc://mcn/help-index', async () => {
777
+ const chunks = getMcnChunks();
778
+ const files = [...new Set(chunks.map((c) => c.relativePath))].sort();
779
+ const stats = getMcnHelpStats();
780
+ const text = `# Bundled Marketing Cloud Next developer API docs (${stats.chunkCount} sections from ${stats.fileCount} files)\n\n` +
781
+ `## Files\n\n` +
782
+ files.map((f) => `- ${f}`).join('\n');
783
+ return {
784
+ contents: [{ uri: 'sfmc://mcn/help-index', mimeType: 'text/markdown', text }],
785
+ };
786
+ });
787
+ // ---------------------------------------------------------------------------
788
+ // Tool: detect_sfmc_platform
789
+ // ---------------------------------------------------------------------------
790
+ server.tool('detect_sfmc_platform', 'Detect the target SFMC platform for a project by checking for sentinel files. ' +
791
+ 'Returns "engagement" if .mcdevrc.json is present, "next" if sfdx-project.json is present, ' +
792
+ 'or "unknown" if neither is found. Call this before writing, validating, or converting code.', {
793
+ projectRoot: z.string().describe('Absolute path to the project root directory to inspect.'),
794
+ }, ({ projectRoot }) => {
795
+ if (fs.existsSync(path.join(projectRoot, '.mcdevrc.json'))) {
796
+ return { content: [{ type: 'text', text: 'engagement' }] };
797
+ }
798
+ if (fs.existsSync(path.join(projectRoot, 'sfdx-project.json'))) {
799
+ return { content: [{ type: 'text', text: 'next' }] };
800
+ }
801
+ return { content: [{ type: 'text', text: 'unknown' }] };
802
+ });
803
+ // ---------------------------------------------------------------------------
804
+ // Tool: search_mcn_help
805
+ // ---------------------------------------------------------------------------
806
+ server.tool('search_mcn_help', 'Search bundled Marketing Cloud Next developer API documentation. ' +
807
+ 'Covers MCN objects, flows, segments, transactional messages, AMPscript behavior in MCN, ' +
808
+ 'and REST/SOAP API references. Use this for MCN developer questions; ' +
809
+ 'use search_mce_help for MCN migration or operational/admin questions.', {
810
+ query: z
811
+ .string()
812
+ .describe('The search query, e.g. "AMPscript FormatDate MCN", "transactional message API", "segment objects".'),
813
+ limit: z
814
+ .number()
815
+ .int()
816
+ .min(1)
817
+ .max(20)
818
+ .optional()
819
+ .describe('Maximum number of result chunks to return (default 5).'),
820
+ }, ({ query, limit = 5 }) => {
821
+ const hits = searchMcnHelp(query, limit);
822
+ if (hits.length === 0) {
823
+ return {
824
+ content: [
825
+ {
826
+ type: 'text',
827
+ text: `No MCN developer docs found for "${query}". The bundled index may not cover this topic — check the live Salesforce developer docs at https://developer.salesforce.com/docs/marketing/marketing-cloud-growth/`,
828
+ },
829
+ ],
830
+ };
831
+ }
832
+ const parts = hits.map(({ chunk }) => {
833
+ const header = `### ${chunk.heading}\n*Source: ${chunk.relativePath}*`;
834
+ return `${header}\n\n${chunk.body}`;
835
+ });
836
+ return {
837
+ content: [
838
+ {
839
+ type: 'text',
840
+ text: `# MCN Developer Docs — "${query}" (${hits.length} results)\n\n${parts.join('\n\n---\n\n')}`,
841
+ },
842
+ ],
843
+ };
844
+ });
845
+ // ---------------------------------------------------------------------------
846
+ // Tool: search_help (unified wrapper — auto-detects platform)
847
+ // ---------------------------------------------------------------------------
848
+ server.tool('search_help', 'Unified help search that automatically detects the target platform (MCE or MCN) from the project ' +
849
+ 'root and routes the query to the right bundled doc index. For MCN projects it searches both the ' +
850
+ 'Marketing Cloud Next developer API reference (`search_mcn_help`) and the MCN operational/admin ' +
851
+ 'help (`search_mce_help` with product_focus:"next") and merges the results. For MCE projects it ' +
852
+ 'searches the full MCE help index (`search_mce_help` with product_focus:"any"). ' +
853
+ 'Pass `projectRoot` to enable auto-detection, or set `target` explicitly to skip detection.', {
854
+ query: z.string().describe('Keywords or question text to search for.'),
855
+ projectRoot: z
856
+ .string()
857
+ .optional()
858
+ .describe('Absolute path to the project root directory. Used to auto-detect the platform by ' +
859
+ 'checking for `.mcdevrc.json` (MCE) or `sfdx-project.json` (MCN). ' +
860
+ 'Omit if you already know the target platform.'),
861
+ target: z
862
+ .enum(['engagement', 'next'])
863
+ .optional()
864
+ .describe('Override the detected platform. `engagement` → MCE help only; `next` → MCN developer ' +
865
+ 'docs + MCN operational help. When both `projectRoot` and `target` are given, ' +
866
+ '`target` takes precedence.'),
867
+ limit: z
868
+ .number()
869
+ .int()
870
+ .min(1)
871
+ .max(20)
872
+ .optional()
873
+ .describe('Maximum total results to return across all searched indexes (default 8).'),
874
+ }, ({ query, projectRoot, target, limit = 8 }) => {
875
+ // Resolve effective platform
876
+ let platform = 'unknown';
877
+ if (target) {
878
+ platform = target;
879
+ }
880
+ else if (projectRoot) {
881
+ if (fs.existsSync(path.join(projectRoot, '.mcdevrc.json')))
882
+ platform = 'engagement';
883
+ else if (fs.existsSync(path.join(projectRoot, 'sfdx-project.json')))
884
+ platform = 'next';
885
+ }
886
+ const sections = [];
887
+ if (platform === 'next') {
888
+ // MCN: search both the developer API reference and the MCN operational/admin help
889
+ const devHits = searchMcnHelp(query, Math.ceil(limit / 2));
890
+ const opsHits = searchMceHelp(query, Math.floor(limit / 2), 'next');
891
+ if (devHits.length > 0) {
892
+ const parts = devHits.map(({ chunk }) => `### ${chunk.heading}\n*Source: ${chunk.relativePath}*\n\n${chunk.body}`);
893
+ sections.push(`## MCN Developer API Docs\n\n${parts.join('\n\n---\n\n')}`);
894
+ }
895
+ if (opsHits.length > 0) {
896
+ const parts = opsHits.map((h, i) => {
897
+ const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
898
+ return (`### ${i + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
899
+ `**Score:** ${h.score}\n\n` +
900
+ `${excerpt}${h.chunk.body.length > 520 ? '…' : ''}`);
901
+ });
902
+ sections.push(`## MCN Operational / Admin Docs\n\n${parts.join('\n\n---\n\n')}`);
903
+ }
904
+ if (sections.length === 0) {
905
+ return {
906
+ content: [
907
+ {
908
+ type: 'text',
909
+ text: `No MCN results found for "${query}". ` +
910
+ 'Check https://developer.salesforce.com/docs/marketing/marketing-cloud-growth/ for developer API content ' +
911
+ 'or https://help.salesforce.com for operational guidance.',
912
+ },
913
+ ],
914
+ };
915
+ }
916
+ }
917
+ else {
918
+ // MCE (or unknown platform — search the full MCE help index)
919
+ const focus = 'any';
920
+ const hits = searchMceHelp(query, limit, focus);
921
+ if (hits.length === 0) {
922
+ const stats = getMceHelpStats();
923
+ const hint = stats.chunkCount === 0
924
+ ? 'Bundled help index missing. Run `npm run bundle-mce-help` from the package folder.'
925
+ : `No results found for "${query}". Try broader keywords.`;
926
+ return { content: [{ type: 'text', text: hint }] };
927
+ }
928
+ const parts = hits.map((h, i) => {
929
+ const excerpt = h.chunk.body.replaceAll(/\s+/g, ' ').slice(0, 520);
930
+ return (`### ${i + 1}. ${h.chunk.relativePath} — ${h.chunk.heading}\n` +
931
+ `**Product:** ${h.chunk.productLabel}\n` +
932
+ `**Score:** ${h.score}\n\n` +
933
+ `${excerpt}${h.chunk.body.length > 520 ? '…' : ''}`);
934
+ });
935
+ sections.push(parts.join('\n\n---\n\n'));
936
+ }
937
+ const platformLabel = platform === 'next'
938
+ ? 'Marketing Cloud Next'
939
+ : platform === 'engagement'
940
+ ? 'Marketing Cloud Engagement'
941
+ : 'all products';
942
+ return {
943
+ content: [
944
+ {
945
+ type: 'text',
946
+ text: `# Help Search — "${query}" (${platformLabel})\n\n${sections.join('\n\n---\n\n')}`,
947
+ },
948
+ ],
949
+ };
950
+ });
951
+ // ---------------------------------------------------------------------------
644
952
  // Prompt: writeAmpscript
645
953
  // ---------------------------------------------------------------------------
646
954
  server.prompt('writeAmpscript', 'Generate AMPscript code for a specific task. Ensures correct syntax, proper use of delimiters, ' +
647
- 'and references to real SFMC functions.', {
955
+ 'and references to real SFMC functions. Supports both Marketing Cloud Engagement and Next targets.', {
648
956
  task: z.string().describe('Description of what the AMPscript code should do.'),
649
957
  context: z
650
958
  .string()
651
959
  .optional()
652
960
  .describe('Optional context about the email, landing page, or SFMC configuration.'),
653
- }, ({ task, context }) => ({
961
+ target: z
962
+ .enum(['engagement', 'next'])
963
+ .optional()
964
+ .describe("Target platform. Use 'next' to restrict output to MCN-supported AMPscript functions only."),
965
+ }, ({ task, context, target }) => ({
654
966
  messages: [
655
967
  {
656
968
  role: 'user',
@@ -658,7 +970,7 @@ server.prompt('writeAmpscript', 'Generate AMPscript code for a specific task. En
658
970
  type: 'text',
659
971
  text: [
660
972
  'You are an expert Salesforce Marketing Cloud developer.',
661
- 'Generate AMPscript code for the following task.',
973
+ `Generate AMPscript code for the following task. Target platform: **${target === 'next' ? 'Marketing Cloud Next (MCN)' : 'Marketing Cloud Engagement (MCE)'}**.`,
662
974
  '',
663
975
  '## Rules',
664
976
  '- Use `%%[ ]%%` for block-level code and `%%= =%%` for inline output.',
@@ -668,6 +980,16 @@ server.prompt('writeAmpscript', 'Generate AMPscript code for a specific task. En
668
980
  '- All function names are case-insensitive but conventionally PascalCase.',
669
981
  '- Do NOT use ES6+ syntax (this is not JavaScript).',
670
982
  '- Validate your output against the AMPscript function catalog.',
983
+ target === 'next'
984
+ ? [
985
+ '',
986
+ '## Marketing Cloud Next (MCN) constraints',
987
+ '- Only use functions available in MCN (API v67.0+). Call `list_ampscript_functions` with `platform: "next"` to verify.',
988
+ '- FormatDate uses Java SimpleDateFormat patterns (not .NET). Example: `yyyy-MM-dd` instead of `yyyy-MM-dd`.',
989
+ '- Lookup requires an even number of search arguments (column/value pairs).',
990
+ '- StringToDate returns a locale-formatted string in MCN — do not chain it with FormatDate.',
991
+ ].join('\n')
992
+ : '',
671
993
  '',
672
994
  `## Task`,
673
995
  task,
@@ -683,37 +1005,55 @@ server.prompt('writeAmpscript', 'Generate AMPscript code for a specific task. En
683
1005
  // Prompt: writeSsjs
684
1006
  // ---------------------------------------------------------------------------
685
1007
  server.prompt('writeSsjs', 'Generate SSJS (Server-Side JavaScript) code for a specific task. Ensures ES5-compatible syntax and ' +
686
- 'correct use of SFMC Platform APIs.', {
1008
+ 'correct use of SFMC Platform APIs. If target is "next", redirects to AMPscript instead.', {
687
1009
  task: z.string().describe('Description of what the SSJS code should do.'),
688
1010
  context: z
689
1011
  .string()
690
1012
  .optional()
691
1013
  .describe('Optional context about the SFMC environment or assets involved.'),
692
- }, ({ task, context }) => ({
1014
+ target: z
1015
+ .enum(['engagement', 'next'])
1016
+ .optional()
1017
+ .describe("Target platform. If 'next', the prompt will explain that SSJS is not supported and suggest AMPscript alternatives."),
1018
+ }, ({ task, context, target }) => ({
693
1019
  messages: [
694
1020
  {
695
1021
  role: 'user',
696
1022
  content: {
697
1023
  type: 'text',
698
- text: [
699
- 'You are an expert Salesforce Marketing Cloud developer.',
700
- 'Generate SSJS code for the following task.',
701
- '',
702
- '## Rules',
703
- '- SSJS runs in an ES5 engine. Use `var`, not `let`/`const`.',
704
- '- No arrow functions, template literals, destructuring, or `class`.',
705
- '- Wrap code in `<script runat="server">` ... `</script>`.',
706
- '- Use `Platform.Load("core", "1.1.5");` before accessing Core library objects.',
707
- '- Use `Platform.Function.*` for SFMC-specific functions (e.g. `Platform.Function.Lookup`).',
708
- '- For SOAP API calls, use WSProxy: `var prox = new WSProxy();`',
709
- '- Use `Platform.Response.Write()` to output content.',
710
- '',
711
- `## Task`,
712
- task,
713
- context ? `\n## Context\n${context}` : '',
714
- ]
715
- .filter(Boolean)
716
- .join('\n'),
1024
+ text: target === 'next'
1025
+ ? [
1026
+ '⚠️ **Marketing Cloud Next (MCN) does not support SSJS.** SSJS is not available in MCN.',
1027
+ '',
1028
+ 'Instead, use **AMPscript** — MCN supports a subset of AMPscript functions (API v67.0+).',
1029
+ 'Call `list_ampscript_functions` with `platform: "next"` to see which functions are available.',
1030
+ 'Then call `writeAmpscript` with `target: "next"` to generate MCN-compatible AMPscript code.',
1031
+ '',
1032
+ `## Original task (for AMPscript rewrite reference)`,
1033
+ task,
1034
+ context ? `\n## Context\n${context}` : '',
1035
+ ]
1036
+ .filter(Boolean)
1037
+ .join('\n')
1038
+ : [
1039
+ 'You are an expert Salesforce Marketing Cloud developer.',
1040
+ 'Generate SSJS code for the following task.',
1041
+ '',
1042
+ '## Rules',
1043
+ '- SSJS runs in an ES5 engine. Use `var`, not `let`/`const`.',
1044
+ '- No arrow functions, template literals, destructuring, or `class`.',
1045
+ '- Wrap code in `<script runat="server">` ... `</script>`.',
1046
+ '- Use `Platform.Load("core", "1.1.5");` before accessing Core library objects.',
1047
+ '- Use `Platform.Function.*` for SFMC-specific functions (e.g. `Platform.Function.Lookup`).',
1048
+ '- For SOAP API calls, use WSProxy: `var prox = new Script.Util.WSProxy();`',
1049
+ '- Use `Platform.Response.Write()` to output content.',
1050
+ '',
1051
+ `## Task`,
1052
+ task,
1053
+ context ? `\n## Context\n${context}` : '',
1054
+ ]
1055
+ .filter(Boolean)
1056
+ .join('\n'),
717
1057
  },
718
1058
  },
719
1059
  ],
@@ -721,17 +1061,25 @@ server.prompt('writeSsjs', 'Generate SSJS (Server-Side JavaScript) code for a sp
721
1061
  // ---------------------------------------------------------------------------
722
1062
  // Prompt: reviewSfmcCode
723
1063
  // ---------------------------------------------------------------------------
724
- server.prompt('reviewSfmcCode', 'Review SFMC code for correctness, best practices, and potential issues. Provides actionable feedback.', {
1064
+ server.prompt('reviewSfmcCode', 'Review SFMC code for correctness, best practices, and potential issues. Provides actionable feedback. ' +
1065
+ "Set target to 'next' to also check for Marketing Cloud Next compatibility.", {
725
1066
  code: z.string().describe('The SFMC code to review.'),
726
1067
  language: z.enum(['ampscript', 'ssjs', 'html', 'auto']).optional(),
727
1068
  focus: z
728
1069
  .string()
729
1070
  .optional()
730
1071
  .describe('Optional focus area, e.g. "security", "performance", "data extension usage".'),
731
- }, ({ code, language = 'auto', focus }) => {
1072
+ target: z
1073
+ .enum(['engagement', 'next'])
1074
+ .optional()
1075
+ .describe("Target platform. Use 'next' to include MCN compatibility in the review (flags unsupported functions and SSJS)."),
1076
+ }, ({ code, language = 'auto', focus, target }) => {
732
1077
  const detectedLang = language === 'auto'
733
1078
  ? detectLanguage(code)
734
1079
  : detectLanguage(code, language);
1080
+ const platformNote = target === 'next'
1081
+ ? '\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).'
1082
+ : '';
735
1083
  return {
736
1084
  messages: [
737
1085
  {
@@ -739,7 +1087,11 @@ server.prompt('reviewSfmcCode', 'Review SFMC code for correctness, best practice
739
1087
  content: {
740
1088
  type: 'text',
741
1089
  text: [
742
- `You are an expert Salesforce Marketing Cloud developer reviewing ${detectedLang.toUpperCase()} code.`,
1090
+ `You are an expert Salesforce Marketing Cloud developer reviewing ${detectedLang.toUpperCase()} code` +
1091
+ (target
1092
+ ? ` for target platform **${target === 'next' ? 'Marketing Cloud Next (MCN)' : 'Marketing Cloud Engagement (MCE)'}**`
1093
+ : '') +
1094
+ '.',
743
1095
  'Identify bugs, anti-patterns, performance issues, and security concerns.',
744
1096
  focus ? `Focus especially on: ${focus}` : '',
745
1097
  '',
@@ -756,14 +1108,14 @@ server.prompt('reviewSfmcCode', 'Review SFMC code for correctness, best practice
756
1108
  '- Correct function names and argument counts',
757
1109
  '- Correct comment syntax (/* */ only)',
758
1110
  '- Proper variable declaration with @',
759
- ].join('\n')
1111
+ ].join('\n') + platformNote
760
1112
  : [
761
1113
  '- No ES6+ syntax (var, not let/const; no arrow functions)',
762
1114
  '- Platform.Load before Core library objects',
763
1115
  '- Correct Platform.Function calls',
764
1116
  '- WSProxy error handling',
765
1117
  '- No sensitive data in logs or responses',
766
- ].join('\n'),
1118
+ ].join('\n') + platformNote,
767
1119
  ]
768
1120
  .filter(Boolean)
769
1121
  .join('\n'),
@@ -775,8 +1127,10 @@ server.prompt('reviewSfmcCode', 'Review SFMC code for correctness, best practice
775
1127
  // ---------------------------------------------------------------------------
776
1128
  // Prompt: convertAmpscriptToSsjs
777
1129
  // ---------------------------------------------------------------------------
778
- server.prompt('convertAmpscriptToSsjs', 'Convert AMPscript code to equivalent SSJS, preserving business logic while adapting to SSJS APIs.', {
779
- ampscript: z.string().describe('The AMPscript code to convert.'),
1130
+ server.prompt('convertAmpscriptToSsjs', 'Convert AMPscript code to equivalent SSJS, preserving business logic while adapting to SSJS APIs. ' +
1131
+ 'Calls the convertAmpscriptToSsjs tool first for deterministic rule-based conversion, ' +
1132
+ 'then applies AI reasoning to handle any MANUAL_REWRITE_REQUIRED sections.', {
1133
+ ampscript: z.string().describe('The AMPscript code to convert to SSJS.'),
780
1134
  }, ({ ampscript }) => ({
781
1135
  messages: [
782
1136
  {
@@ -784,19 +1138,25 @@ server.prompt('convertAmpscriptToSsjs', 'Convert AMPscript code to equivalent SS
784
1138
  content: {
785
1139
  type: 'text',
786
1140
  text: [
787
- 'Convert the following AMPscript code to equivalent SSJS.',
1141
+ 'You are an expert Salesforce Marketing Cloud developer converting AMPscript to SSJS.',
1142
+ '',
1143
+ '## Instructions',
1144
+ '1. Call the `convertAmpscriptToSsjs` **tool** with the code below to get a deterministic conversion.',
1145
+ '2. Review the `flaggedSections` in the result — these are constructs the tool could not convert automatically.',
1146
+ '3. For each flagged section, apply your expertise:',
1147
+ ' - Email-specific functions (ContentArea, TreatAsContent, etc.) → use equivalent SSJS Content or Server.EscapeJavaScript approaches',
1148
+ ' - Personalization strings (%%FirstName%% etc.) → use Platform.Variable/Recipient equivalents',
1149
+ ' - AMPscript-only data lookups → translate to SSJS DataExtension / WSProxy where appropriate',
1150
+ '4. Produce a single final SSJS code block.',
1151
+ '5. Add a short change log as a bulleted list.',
788
1152
  '',
789
- '## Conversion rules',
790
- '- AMPscript `Lookup()` `Platform.Function.Lookup()` in SSJS',
791
- '- AMPscript `LookupRows()` → `Platform.Function.LookupRows()` in SSJS',
792
- '- AMPscript `@variable` → `var variable` in SSJS',
793
- '- AMPscript `SET @x = value` → `var x = value;` in SSJS',
794
- '- AMPscript `IF @x == "y" THEN` → `if (x === "y") {` in SSJS',
795
- '- AMPscript `OUTPUT(CONCAT(...))` → `Platform.Response.Write(...)` in SSJS',
796
- '- AMPscript `FOR @i = 1 TO 10 DO` → `for (var i = 1; i <= 10; i++) {` in SSJS',
797
- '- Use `var`, not `let`/`const`. No arrow functions or template literals.',
1153
+ '## SSJS rules',
1154
+ '- Use `var`, not `let`/`const`. No arrow functions, template literals, or destructuring.',
798
1155
  '- Wrap in `<script runat="server">...</script>`.',
799
- '- Add `Platform.Load("core", "1.1.5");` if using DataExtension, Rows, etc.',
1156
+ '- Add `Platform.Load("Core", "1.1.5");` if using DataExtension, Rows, etc.',
1157
+ '- AMPscript functions → `Platform.Function.*` equivalents.',
1158
+ '- AMPscript `@variable` → bare `variable` in SSJS.',
1159
+ '- AMPscript `Output` / `OutputLine` → `Platform.Response.Write()`.',
800
1160
  '',
801
1161
  '## AMPscript to convert',
802
1162
  '```ampscript',
@@ -869,6 +1229,414 @@ server.prompt('answerMceHowTo', 'Answer a Marketing Cloud **administration or se
869
1229
  };
870
1230
  });
871
1231
  // ---------------------------------------------------------------------------
1232
+ // Tool: check_mcn_compatibility
1233
+ // ---------------------------------------------------------------------------
1234
+ server.tool('check_mcn_compatibility', 'Analyze one or more AMPscript/HTML files for Marketing Cloud Next (MCN) readiness. ' +
1235
+ 'Returns an executive summary and a per-file, per-function report with migration difficulty. ' +
1236
+ 'SSJS blocks that only use Platform.Function.* calls are classified as "Needs conversion" (not "Not migratable"). ' +
1237
+ 'Use this tool before using rewrite_for_mcn.', {
1238
+ files: z
1239
+ .array(z.object({
1240
+ filename: z.string().describe('File name (e.g. "email-template.html").'),
1241
+ content: z.string().describe('Full file content to analyze.'),
1242
+ }))
1243
+ .describe('List of files to analyze.'),
1244
+ }, ({ files }) => {
1245
+ const results = [];
1246
+ for (const { filename, content } of files) {
1247
+ const ampFunctions = [];
1248
+ const ssjsBlocks = [];
1249
+ // 1. Extract and classify AMPscript function calls
1250
+ const callSites = extractAmpscriptFunctionCalls(content);
1251
+ for (const site of callSites) {
1252
+ const mcnSince = getMcnApiVersion(site.name);
1253
+ const notes = getMcnNotes(site.name);
1254
+ let status;
1255
+ let reason;
1256
+ if (mcnSince !== null && notes === null) {
1257
+ status = 'supported';
1258
+ reason = '—';
1259
+ }
1260
+ else if (mcnSince !== null && notes !== null) {
1261
+ status = 'needs-review';
1262
+ reason = notes;
1263
+ }
1264
+ else {
1265
+ status = 'not-supported';
1266
+ reason = 'No MCN equivalent';
1267
+ // Refine for CloudPages-specific functions
1268
+ if (CLOUDPAGES_ONLY_FUNCTIONS.has(site.name.toLowerCase())) {
1269
+ reason = `${site.name}() is a CloudPages-specific function (not available in MCN)`;
1270
+ }
1271
+ }
1272
+ ampFunctions.push({ name: site.name, line: site.line + 1, status, reason });
1273
+ }
1274
+ // 2. Detect and classify SSJS blocks
1275
+ const ssjsBlockPattern = /<script[^>]+runat=['"]?server['"]?[^>]*>([\s\S]*?)<\/script>/gi;
1276
+ let blockMatch;
1277
+ let blockIndex = 0;
1278
+ while ((blockMatch = ssjsBlockPattern.exec(content)) !== null) {
1279
+ blockIndex++;
1280
+ const blockCode = blockMatch[1];
1281
+ const lineApprox = content.slice(0, blockMatch.index).split('\n').length;
1282
+ // Check for non-migratable patterns
1283
+ let notMigratableReason = '';
1284
+ for (const { pattern, reason } of NON_MIGRATABLE_SSJS_PATTERNS) {
1285
+ pattern.lastIndex = 0;
1286
+ if (pattern.test(blockCode)) {
1287
+ notMigratableReason = reason;
1288
+ break;
1289
+ }
1290
+ }
1291
+ if (notMigratableReason) {
1292
+ ssjsBlocks.push({
1293
+ index: blockIndex,
1294
+ lineApprox,
1295
+ status: 'not-migratable',
1296
+ reason: notMigratableReason,
1297
+ });
1298
+ }
1299
+ else {
1300
+ ssjsBlocks.push({
1301
+ index: blockIndex,
1302
+ lineApprox,
1303
+ status: 'needs-conversion',
1304
+ reason: 'Contains only convertible SSJS patterns — use convertSsjsToAmpscript',
1305
+ });
1306
+ }
1307
+ }
1308
+ // 3. Assess per-file difficulty
1309
+ const hasCloudPagesFn = ampFunctions.some((f) => CLOUDPAGES_ONLY_FUNCTIONS.has(f.name.toLowerCase()) &&
1310
+ f.status === 'not-supported');
1311
+ const hasNotMigratableSsjs = ssjsBlocks.some((b) => b.status === 'not-migratable');
1312
+ const hasUnsupportedAmp = ampFunctions.some((f) => f.status === 'not-supported' &&
1313
+ !CLOUDPAGES_ONLY_FUNCTIONS.has(f.name.toLowerCase()));
1314
+ const hasConvertibleSsjs = ssjsBlocks.some((b) => b.status === 'needs-conversion');
1315
+ const hasNeedsReview = ampFunctions.some((f) => f.status === 'needs-review');
1316
+ let difficulty;
1317
+ if (hasCloudPagesFn || hasNotMigratableSsjs) {
1318
+ difficulty = 'not-migratable';
1319
+ }
1320
+ else if (hasUnsupportedAmp || hasConvertibleSsjs) {
1321
+ difficulty = 'significant';
1322
+ }
1323
+ else if (hasNeedsReview) {
1324
+ difficulty = 'minor';
1325
+ }
1326
+ else {
1327
+ difficulty = 'ready';
1328
+ }
1329
+ results.push({ filename, difficulty, ampFunctions, ssjsBlocks });
1330
+ }
1331
+ // 4. Build Markdown report
1332
+ const difficultyLabel = {
1333
+ ready: 'Ready',
1334
+ minor: 'Minor changes needed',
1335
+ significant: 'Significant rewrite required',
1336
+ 'not-migratable': 'Not migratable',
1337
+ };
1338
+ const counts = {
1339
+ ready: results.filter((r) => r.difficulty === 'ready').length,
1340
+ minor: results.filter((r) => r.difficulty === 'minor').length,
1341
+ significant: results.filter((r) => r.difficulty === 'significant').length,
1342
+ notMigratable: results.filter((r) => r.difficulty === 'not-migratable').length,
1343
+ };
1344
+ const totalFiles = results.length;
1345
+ const effortLevels = counts.notMigratable > 0
1346
+ ? 'Not possible'
1347
+ : counts.significant > 0
1348
+ ? 'Hard'
1349
+ : counts.minor > 0
1350
+ ? 'Medium'
1351
+ : 'Easy';
1352
+ const summaryLines = [
1353
+ '## MCN Compatibility Report',
1354
+ '',
1355
+ '### Executive Summary',
1356
+ `- ${counts.ready}/${totalFiles} files: Ready`,
1357
+ `- ${counts.minor}/${totalFiles} files: Minor changes needed`,
1358
+ `- ${counts.significant}/${totalFiles} files: Significant rewrite required`,
1359
+ `- ${counts.notMigratable}/${totalFiles} files: Not migratable`,
1360
+ '',
1361
+ `Overall migration effort: **${effortLevels}**`,
1362
+ '',
1363
+ '---',
1364
+ ];
1365
+ const fileLines = [];
1366
+ for (const result of results) {
1367
+ const label = difficultyLabel[result.difficulty] ?? result.difficulty;
1368
+ fileLines.push('', `### ${result.filename} — ${label}`, '');
1369
+ if (result.ssjsBlocks.length > 0) {
1370
+ fileLines.push('**SSJS Blocks:**', '');
1371
+ for (const block of result.ssjsBlocks) {
1372
+ const icon = block.status === 'needs-conversion' ? '🔄' : '🚫';
1373
+ const statusLabel = block.status === 'needs-conversion' ? 'Needs conversion' : 'Not migratable';
1374
+ fileLines.push(`${icon} SSJS block ${block.index} (≈line ${block.lineApprox}): **${statusLabel}** — ${block.reason}`);
1375
+ }
1376
+ fileLines.push('');
1377
+ }
1378
+ if (result.ampFunctions.length > 0) {
1379
+ fileLines.push('| Function | Line | Status | Reason |', '|---|---|---|---|');
1380
+ for (const fn of result.ampFunctions) {
1381
+ const icon = fn.status === 'supported'
1382
+ ? '✅'
1383
+ : fn.status === 'needs-review'
1384
+ ? '⚠️'
1385
+ : '❌';
1386
+ const statusLabel = fn.status === 'supported'
1387
+ ? 'Supported'
1388
+ : fn.status === 'needs-review'
1389
+ ? 'Needs review'
1390
+ : 'Not supported';
1391
+ fileLines.push(`| ${fn.name} | ${fn.line} | ${icon} ${statusLabel} | ${fn.reason} |`);
1392
+ }
1393
+ }
1394
+ else if (result.ssjsBlocks.length === 0) {
1395
+ fileLines.push('*No AMPscript functions or SSJS blocks found.*');
1396
+ }
1397
+ fileLines.push('', '---');
1398
+ }
1399
+ return {
1400
+ content: [
1401
+ {
1402
+ type: 'text',
1403
+ text: [...summaryLines, ...fileLines].join('\n'),
1404
+ },
1405
+ ],
1406
+ };
1407
+ });
1408
+ // ---------------------------------------------------------------------------
1409
+ // Tool: rewrite_for_mcn
1410
+ // ---------------------------------------------------------------------------
1411
+ server.tool('rewrite_for_mcn', 'Deterministically rewrite AMPscript (and optionally SSJS) code for Marketing Cloud Next compatibility. ' +
1412
+ 'Handles: FormatDate(StringToDate(x)) simplification, .NET→Java format strings, ' +
1413
+ 'MCE-only function annotations, SSJS→AMPscript rule-based conversion. ' +
1414
+ 'Flags complex constructs as MANUAL_REWRITE_REQUIRED. ' +
1415
+ 'Use the rewrite_for_mcn PROMPT (not this tool) for AI-enhanced handling of MANUAL_REWRITE_REQUIRED sections.', {
1416
+ code: z.string().describe('The AMPscript or HTML code to rewrite for MCN.'),
1417
+ context: z
1418
+ .enum(['email', 'cloudpage', 'auto'])
1419
+ .optional()
1420
+ .default('auto')
1421
+ .describe("Content context. Use 'cloudpage' to immediately flag as not migratable. Default 'auto' detects context."),
1422
+ }, ({ code, context = 'auto' }) => {
1423
+ // CloudPage detection
1424
+ const isCloudPage = context === 'cloudpage' ||
1425
+ (context === 'auto' &&
1426
+ /\b(CloudPagesURL|RequestParameter|QueryParameter)\s*\(/i.test(code));
1427
+ if (isCloudPage) {
1428
+ return {
1429
+ content: [
1430
+ {
1431
+ type: 'text',
1432
+ text: JSON.stringify({
1433
+ rewrittenCode: code,
1434
+ changes: [],
1435
+ nonMigratableItems: [
1436
+ {
1437
+ line: 1,
1438
+ code: 'CloudPage context detected',
1439
+ reason: 'CloudPages (RequestParameter, QueryParameter, CloudPagesURL, Redirect) are not available in Marketing Cloud Next. This use case cannot be migrated.',
1440
+ },
1441
+ ],
1442
+ difficulty: 'not-migratable',
1443
+ summary: 'CloudPage context — not available in Marketing Cloud Next',
1444
+ }),
1445
+ },
1446
+ ],
1447
+ };
1448
+ }
1449
+ // Rewrite AMPscript portions
1450
+ const ampResult = rewriteAmpForMcn(code, {
1451
+ isMcnSupportedFn: isMcnSupported,
1452
+ getMcnNotesFn: getMcnNotes,
1453
+ });
1454
+ let finalCode = ampResult.rewrittenCode;
1455
+ const allChanges = [...ampResult.changes];
1456
+ const allNonMigratable = [...ampResult.nonMigratableItems];
1457
+ // Convert SSJS blocks to AMPscript
1458
+ const ssjsPattern = /<script[^>]+runat=['"]?server['"]?[^>]*>[\s\S]*?<\/script>/gi;
1459
+ finalCode = finalCode.replaceAll(ssjsPattern, (ssjsBlock) => {
1460
+ if (!isSsjsBlockConvertible(ssjsBlock)) {
1461
+ allNonMigratable.push({
1462
+ line: 0,
1463
+ code: ssjsBlock.slice(0, 100),
1464
+ reason: 'SSJS block contains non-migratable constructs',
1465
+ });
1466
+ return (ssjsBlock +
1467
+ '\n%%-- MANUAL_REWRITE_REQUIRED: Non-migratable SSJS block above --%% ');
1468
+ }
1469
+ const ssjsResult = ssjsToAmpscript(ssjsBlock);
1470
+ for (const c of ssjsResult.changes) {
1471
+ allChanges.push({ line: c.line, type: 'rewritten', description: c.description });
1472
+ }
1473
+ for (const f of ssjsResult.flaggedSections) {
1474
+ allNonMigratable.push({ line: f.line, code: f.code, reason: f.reason });
1475
+ }
1476
+ return ssjsResult.convertedCode;
1477
+ });
1478
+ // Reassess difficulty
1479
+ const hasMigratable = allNonMigratable.length > 0;
1480
+ const difficulty = hasMigratable && allNonMigratable.some((i) => i.reason.includes('not-migratable'))
1481
+ ? 'not-migratable'
1482
+ : ampResult.difficulty;
1483
+ const result = {
1484
+ rewrittenCode: finalCode,
1485
+ changes: allChanges,
1486
+ nonMigratableItems: allNonMigratable,
1487
+ difficulty,
1488
+ summary: ampResult.summary,
1489
+ };
1490
+ return {
1491
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1492
+ };
1493
+ });
1494
+ // ---------------------------------------------------------------------------
1495
+ // Prompt: rewrite_for_mcn
1496
+ // ---------------------------------------------------------------------------
1497
+ server.prompt('rewrite_for_mcn', 'Rewrite AMPscript/SSJS code to be compatible with Marketing Cloud Next. ' +
1498
+ 'Calls the rewrite_for_mcn tool first for deterministic rewrites, then applies ' +
1499
+ 'AI reasoning to handle any MANUAL_REWRITE_REQUIRED sections.', {
1500
+ code: z.string().describe('The AMPscript or HTML code to rewrite for MCN.'),
1501
+ context: z
1502
+ .enum(['email', 'cloudpage', 'auto'])
1503
+ .optional()
1504
+ .describe("Content context — use 'cloudpage' to immediately flag as not migratable."),
1505
+ }, ({ code, context = 'auto' }) => ({
1506
+ messages: [
1507
+ {
1508
+ role: 'user',
1509
+ content: {
1510
+ type: 'text',
1511
+ text: [
1512
+ 'You are an expert Salesforce Marketing Cloud developer helping migrate code to Marketing Cloud Next (MCN).',
1513
+ '',
1514
+ '## Instructions',
1515
+ '1. Call the `rewrite_for_mcn` **tool** with the code and context below.',
1516
+ '2. Review the `nonMigratableItems` in the result.',
1517
+ '3. For each item marked `MANUAL_REWRITE_REQUIRED`, attempt an AI-driven conversion:',
1518
+ ' - Complex loops → AMPscript FOR/NEXT blocks if possible',
1519
+ ' - SSJS try/catch → AMPscript RaiseError() with conditional guards',
1520
+ ' - Array methods (forEach, map, filter) → AMPscript FOR loops over RowSets',
1521
+ ' - JSON.parse/stringify → BuildRowsetFromJson() / row operations',
1522
+ '4. Produce a single final rewritten code block.',
1523
+ '5. Include a concise change log as a bulleted list.',
1524
+ '',
1525
+ '## MCN rules to enforce',
1526
+ '- AMPscript only — no SSJS blocks (`<script runat="server">`) in the final output.',
1527
+ '- FormatDate() must use Java SimpleDateFormat strings, not .NET strings.',
1528
+ '- FormatDate(StringToDate(x), fmt) must be simplified to FormatDate(x, fmt).',
1529
+ '- Lookup() must have an even number of search column/value pairs.',
1530
+ '- Functions not in the MCN catalog must be replaced or removed.',
1531
+ '- CloudPages context (RequestParameter, QueryParameter, CloudPagesURL) → cannot migrate; explain why.',
1532
+ '',
1533
+ `## Context: ${context}`,
1534
+ '',
1535
+ '## Code to rewrite',
1536
+ '```',
1537
+ code,
1538
+ '```',
1539
+ ].join('\n'),
1540
+ },
1541
+ },
1542
+ ],
1543
+ }));
1544
+ // ---------------------------------------------------------------------------
1545
+ // Tool: convertSsjsToAmpscript
1546
+ // ---------------------------------------------------------------------------
1547
+ server.tool('convertSsjsToAmpscript', 'Deterministically convert SSJS (Server-Side JavaScript) code to AMPscript using rule-based transformations. ' +
1548
+ 'Handles: Platform.Function.* → AMPscript equivalents, Platform.Variable.GetValue/SetValue → @variable, ' +
1549
+ 'Platform.Response.Write → OutputLine, var declarations → SET, if/else → IF/ELSE/ENDIF. ' +
1550
+ 'Flags JS-native constructs (try/catch, array methods, JSON, regex) as MANUAL_REWRITE_REQUIRED. ' +
1551
+ 'Use the convertSsjsToAmpscript PROMPT for AI-enhanced handling of flagged sections.', {
1552
+ code: z
1553
+ .string()
1554
+ .describe('The SSJS code to convert (may include <script runat="server"> tags).'),
1555
+ }, ({ code }) => {
1556
+ const result = ssjsToAmpscript(code);
1557
+ return {
1558
+ content: [
1559
+ {
1560
+ type: 'text',
1561
+ text: JSON.stringify({
1562
+ convertedCode: result.convertedCode,
1563
+ changes: result.changes,
1564
+ flaggedSections: result.flaggedSections,
1565
+ }, null, 2),
1566
+ },
1567
+ ],
1568
+ };
1569
+ });
1570
+ // ---------------------------------------------------------------------------
1571
+ // Prompt: convertSsjsToAmpscript
1572
+ // ---------------------------------------------------------------------------
1573
+ server.prompt('convertSsjsToAmpscript', 'Convert SSJS (Server-Side JavaScript) code to equivalent AMPscript. ' +
1574
+ 'Calls the convertSsjsToAmpscript tool first for deterministic rule-based conversion, ' +
1575
+ 'then applies AI reasoning to handle any MANUAL_REWRITE_REQUIRED sections.', {
1576
+ code: z.string().describe('The SSJS code to convert to AMPscript.'),
1577
+ }, ({ code }) => ({
1578
+ messages: [
1579
+ {
1580
+ role: 'user',
1581
+ content: {
1582
+ type: 'text',
1583
+ text: [
1584
+ 'You are an expert Salesforce Marketing Cloud developer converting SSJS to AMPscript.',
1585
+ '',
1586
+ '## Instructions',
1587
+ '1. Call the `convertSsjsToAmpscript` **tool** with the code below to get a deterministic conversion.',
1588
+ '2. Review the `flaggedSections` in the result — these are constructs the tool could not convert automatically.',
1589
+ '3. For each flagged section, apply your expertise:',
1590
+ ' - SSJS try/catch → use RaiseError() and conditional guards in AMPscript',
1591
+ ' - Array .forEach/.map → AMPscript FOR @i = 1 TO RowCount(@rs) DO loops',
1592
+ ' - JSON.parse/stringify → use BuildRowsetFromJson() or Field() on RowSets',
1593
+ ' - HTTP calls → use HTTPGet() / HTTPPost() in AMPscript',
1594
+ ' - Complex string manipulation → AMPscript string functions (Concat, Replace, Substring, etc.)',
1595
+ '4. Produce a single final AMPscript code block.',
1596
+ '5. Add a short change log as a bulleted list.',
1597
+ '',
1598
+ '## AMPscript syntax reminders',
1599
+ '- Blocks: `%%[ ... ]%%` — statements: `SET @x = expr`',
1600
+ '- Variables: `@varName` (no declaration needed except VAR)',
1601
+ '- Functions: PascalCase, no Platform.Function. prefix',
1602
+ '- Conditions: `IF cond THEN ... ELSEIF cond THEN ... ELSE ... ENDIF`',
1603
+ '- Loops: `FOR @i = 1 TO @count DO ... NEXT @i`',
1604
+ '- Output: `%%=Output(@x)=%%` (inline) or `%%[ OutputLine(x) ]%%` (block)',
1605
+ '',
1606
+ '## SSJS code to convert',
1607
+ '```javascript',
1608
+ code,
1609
+ '```',
1610
+ ].join('\n'),
1611
+ },
1612
+ },
1613
+ ],
1614
+ }));
1615
+ // ---------------------------------------------------------------------------
1616
+ // Tool: convertAmpscriptToSsjs
1617
+ // ---------------------------------------------------------------------------
1618
+ server.tool('convertAmpscriptToSsjs', 'Deterministically convert AMPscript code to equivalent SSJS using rule-based transformations. ' +
1619
+ 'Handles: SET @x → var x, IF/ELSEIF/ELSE/ENDIF → JS conditionals, FOR/NEXT → for loops, ' +
1620
+ 'Output/OutputLine → Platform.Response.Write, AMPscript functions → Platform.Function equivalents. ' +
1621
+ 'Flags AMPscript-only constructs (email-specific functions, personalization strings) as MANUAL_REWRITE_REQUIRED. ' +
1622
+ 'Use the convertAmpscriptToSsjs PROMPT for AI-enhanced handling of flagged sections.', {
1623
+ code: z.string().describe('The AMPscript code to convert to SSJS.'),
1624
+ }, ({ code }) => {
1625
+ const result = ampscriptToSsjs(code);
1626
+ return {
1627
+ content: [
1628
+ {
1629
+ type: 'text',
1630
+ text: JSON.stringify({
1631
+ convertedCode: result.convertedCode,
1632
+ changes: result.changes,
1633
+ flaggedSections: result.flaggedSections,
1634
+ }, null, 2),
1635
+ },
1636
+ ],
1637
+ };
1638
+ });
1639
+ // ---------------------------------------------------------------------------
872
1640
  // Start
873
1641
  // ---------------------------------------------------------------------------
874
1642
  async function main() {