mcp-server-sfmc 1.9.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mcp-server-sfmc
2
2
 
3
- MCP server providing Salesforce Marketing Cloud language intelligence — AMPscript, SSJS, and GTL — as Model Context Protocol tools, resources, and prompts for AI-assisted development and code review. Ships two searchable doc indexes: **MCE help** (Salesforce Help for Marketing Cloud Engagement administration and setup) and **MCN developer docs** (Marketing Cloud Next REST API and developer reference). Includes full **Marketing Cloud Next (MCN) platform awareness**: auto-detects whether a project targets MCE or MCN, restricts completions and validation to MCN-supported functions, flags SSJS as unsupported in MCN, surfaces MCN behavioral differences in hover and diagnostics, and provides a **migration toolkit** to analyze and rewrite code for MCN.
3
+ MCP server providing Salesforce Marketing Cloud language intelligence — AMPscript, SSJS, GTL, and **Marketing Cloud Next (MCN) Handlebars** — as Model Context Protocol tools, resources, and prompts for AI-assisted development and code review. Ships two searchable doc indexes: **MCE help** (Salesforce Help for Marketing Cloud Engagement administration and setup) and **MCN developer docs** (Marketing Cloud Next REST API and developer reference). Includes full **Marketing Cloud Next (MCN) platform awareness**: auto-detects whether a project targets MCE or MCN, restricts completions and validation to MCN-supported functions, flags SSJS as unsupported in MCN, surfaces MCN behavioral differences in hover and diagnostics, validates MCN Handlebars templates, and provides a **migration toolkit** to analyze and rewrite code for MCN — including AMPscript ↔ Handlebars and SSJS → Handlebars conversion.
4
4
 
5
5
  Built on [sfmc-language-lsp](https://github.com/JoernBerkefeld/sfmc-language-lsp), the same engine that powers the [SFMC Language Service VS Code extension](https://marketplace.visualstudio.com/items?itemName=joernberkefeld.sfmc-language).
6
6
 
@@ -41,7 +41,7 @@ Ready-to-copy workflows (GitHub Actions, GitLab CI, Jenkins, Azure Pipelines, Bi
41
41
 
42
42
  | Feature | Details |
43
43
  | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | **Validation** | Syntax errors, unknown functions, arity mismatches, unsupported SSJS syntax; `target: 'next'` flags MCN-incompatible functions and all SSJS |
44
+ | **Validation** | Syntax errors, unknown functions, arity mismatches, unsupported SSJS syntax; `target: 'next'` flags MCN-incompatible functions and all SSJS; `validate_handlebars` checks MCN Handlebars helper names, arity, block balance, and unsupported constructs |
45
45
  | **Lookup** | Full function signatures, parameters, return types, MCN compatibility badge (API version), behavioral notes, and examples |
46
46
  | **PR review** | Diff-aware review tool that surfaces issues in the exact lines that changed |
47
47
  | **Fix suggestions** | Concrete, compilable replacement code; `target: 'next'` includes MCN platform issues |
@@ -50,8 +50,10 @@ Ready-to-copy workflows (GitHub Actions, GitLab CI, Jenkins, Azure Pipelines, Bi
50
50
  | **MCN compatibility analysis** | `check_mcn_compatibility` analyzes files for MCN readiness — per-function classification (supported / needs review / not supported), SSJS block migration difficulty, and an executive summary |
51
51
  | **MCN migration** | `rewrite_for_mcn` (tool + prompt) deterministically rewrites AMPscript and converts convertible SSJS to AMPscript for MCN, then applies AI reasoning to remaining manual-rewrite sections |
52
52
  | **Code conversion** | `convertSsjsToAmpscript` and `convertAmpscriptToSsjs` (tool + prompt hybrid) — rule-based conversion with AI-enhanced handling of flagged sections |
53
- | **Prompts** | Guided prompts for writing AMPscript/SSJS (with MCN constraints), reviewing code, converting between the two, and rewriting for MCN |
54
- | **Resources** | Full function catalogs, keyword list, unsupported ES6+ syntax list |
53
+ | **MCN Handlebars** | `lookup_handlebars_helper`, `list_handlebars_helpers`, `get_handlebars_completions`, and `write_handlebars` (authoring) backed by the `handlebars-data` catalog |
54
+ | **Handlebars conversion** | `convertAmpscriptToHandlebars`, `convertHandlebarsToAmpscript`, and `convertSsjsToHandlebars` (tool + prompt hybrid) — data-driven from `ampscript-data` `handlebarsEquivalent` / `mcnHandlebarsGap` |
55
+ | **Prompts** | Guided prompts for writing AMPscript/SSJS/Handlebars (with MCN constraints), reviewing code, converting between languages, and rewriting for MCN |
56
+ | **Resources** | Full function catalogs, keyword list, unsupported ES6+ syntax list, MCN Handlebars helper and binding catalogs |
55
57
  | **Help search** | `search_help` (unified, auto-detects MCE vs MCN from project root); `search_mce_help` (MCE help, 7 product scopes); `search_mcn_help` (MCN developer API reference) |
56
58
 
57
59
  ## Connecting AI clients
@@ -156,6 +158,14 @@ Then replace `"command": "npx", "args": ["-y", "mcp-server-sfmc@latest"]` with:
156
158
  | `rewrite_for_mcn` | Deterministic MCN rewrite engine: fixes `.NET → Java` format strings, removes `StringToDate` wrappers, converts convertible SSJS to AMPscript, marks unsupported functions, annotates the rest as `MANUAL_REWRITE_REQUIRED`. **Prefer the `rewrite_for_mcn` prompt** for interactive use — it calls this tool first, then applies AI reasoning to the flagged sections. Use the tool directly only for structured JSON output in pipelines. |
157
159
  | `convertSsjsToAmpscript` | Rule-based SSJS → AMPscript conversion engine: `Platform.Function.*` → AMPscript equivalents, variable/request access, control flow. Flags JS-native constructs as `MANUAL_REWRITE_REQUIRED`. **Prefer the `convertSsjsToAmpscript` prompt** for interactive use. |
158
160
  | `convertAmpscriptToSsjs` | Rule-based AMPscript → SSJS conversion engine: variables, control flow, function calls. Flags AMPscript-only constructs as `MANUAL_REWRITE_REQUIRED`. **Prefer the `convertAmpscriptToSsjs` prompt** for interactive use. |
161
+ | `validate_handlebars` | Validate MCN Handlebars template code — helper names, arity, block balance, and unsupported constructs (partials, decorators, built-in helpers absent from the locked-down MCN engine). |
162
+ | `lookup_handlebars_helper` | Signature, parameters, description, origin, and MCN API version for a single MCN Handlebars helper (case-insensitive). |
163
+ | `list_handlebars_helpers` | List all MCN Handlebars helpers, optionally filtered by `category` and/or `origin` (`handlebars-builtin`, `mcn-helper`, `mcn-platform`). |
164
+ | `get_handlebars_completions` | List MCN Handlebars helper completions, optionally filtered by prefix. |
165
+ | `convertAmpscriptToHandlebars` | Rule-based AMPscript → MCN Handlebars conversion. Maps functions to helpers where an equivalent exists; flags unsupported functions and runtime gaps as `MANUAL_REWRITE_REQUIRED`. **Prefer the `convertAmpscriptToHandlebars` prompt** for interactive use. |
166
+ | `convertHandlebarsToAmpscript` | Rule-based MCN Handlebars → AMPscript conversion. Maps helpers and bindings back to AMPscript; flags block helpers as `MANUAL_REWRITE_REQUIRED`. **Prefer the `convertHandlebarsToAmpscript` prompt** for interactive use. |
167
+ | `convertSsjsToHandlebars` | Transitive SSJS → AMPscript → MCN Handlebars conversion. Conservatively flags imperative SSJS for manual rewrite. **Prefer the `convertSsjsToHandlebars` prompt** for interactive use. |
168
+ | `write_handlebars` | Validate an MCN Handlebars-in-HTML draft against the helper catalog. **Prefer the `writeHandlebars` prompt** for authoring. |
159
169
  | `search_help` | **Unified help search** — auto-detects the platform from `projectRoot` and routes to the right doc index. MCN projects search both the developer API reference and MCN admin docs. Pass `target` to override detection. |
160
170
  | `search_mce_help` | Search bundled Marketing Cloud Engagement setup/ops help; use `product_focus` to target Engagement vs Next |
161
171
  | `search_mcn_help` | Search bundled Marketing Cloud Next developer API documentation (objects, flows, segments, REST/SOAP APIs, AMPscript behavior in MCN) |
@@ -168,6 +178,8 @@ Then replace `"command": "npx", "args": ["-y", "mcp-server-sfmc@latest"]` with:
168
178
  | `sfmc://ssjs/functions` | Full SSJS function catalog |
169
179
  | `sfmc://ampscript/keywords` | All AMPscript keywords |
170
180
  | `sfmc://ssjs/unsupported-syntax` | ES6+ features not supported in SFMC SSJS |
181
+ | `sfmc://handlebars/helpers` | Full MCN Handlebars helper catalog with signatures, origin, and MCN API version |
182
+ | `sfmc://handlebars/bindings` | MCN Handlebars data bindings (context variables available in templates) |
171
183
  | `sfmc://mce/product-context` | How **Marketing Cloud Engagement** differs from **Marketing Cloud Next** (when to use which) |
172
184
  | `sfmc://mce/help-index` | List of bundled MCE help files and section counts per product scope |
173
185
  | `sfmc://mcn/help-index` | List of bundled MCN developer API doc files and chunk counts |
@@ -184,6 +196,10 @@ Access via `/mcp.sfmc.writeAmpscript` etc. in VS Code, or via the prompts API:
184
196
  | `rewrite_for_mcn` | **Primary interface for MCN migration.** Internally calls the `rewrite_for_mcn` tool for all deterministic rewrites, then applies AI reasoning to every `MANUAL_REWRITE_REQUIRED` section — producing a single fully migrated code block with a prose changelog. You always get both the rule-based pass and the AI pass in one step. |
185
197
  | `convertSsjsToAmpscript` | **Primary interface for SSJS → AMPscript conversion.** Internally calls the `convertSsjsToAmpscript` tool for rule-based conversion, then AI-reasons over any sections the rules could not handle. |
186
198
  | `convertAmpscriptToSsjs` | **Primary interface for AMPscript → SSJS conversion.** Internally calls the `convertAmpscriptToSsjs` tool for rule-based conversion, then applies AI reasoning to flagged AMPscript-only constructs. |
199
+ | `writeHandlebars` | Generate MCN Handlebars-in-HTML for a described task, constrained to the locked-down MCN helper catalog. Validates the draft against `handlebars-data` and reports unsupported constructs. |
200
+ | `convertAmpscriptToHandlebars` | **Primary interface for AMPscript → MCN Handlebars conversion.** Internally calls the `convertAmpscriptToHandlebars` tool for rule-based conversion, then AI-reasons over functions with no Handlebars equivalent (runtime gaps and unsupported functions). |
201
+ | `convertHandlebarsToAmpscript` | **Primary interface for MCN Handlebars → AMPscript conversion.** Internally calls the `convertHandlebarsToAmpscript` tool for rule-based conversion, then AI-reasons over block helpers and constructs the rules could not handle. |
202
+ | `convertSsjsToHandlebars` | **Primary interface for SSJS → MCN Handlebars conversion.** Transitive (SSJS → AMPscript → Handlebars); calls the `convertSsjsToHandlebars` tool, then applies AI reasoning to imperative SSJS flagged for manual rewrite. |
187
203
  | `answerMceHowTo` | Guided prompt for admin/setup questions — searches bundled help and keeps Engagement vs Next explicit |
188
204
 
189
205
  ## Migrating code to Marketing Cloud Next
@@ -243,6 +259,34 @@ The same hybrid pattern applies to `convertSsjsToAmpscript` and `convertAmpscrip
243
259
 
244
260
  Functions that depend on CloudPages context (`CloudPagesURL`, `RequestParameter`, `QueryParameter`, `Redirect`, `MicrositeURL`) have no equivalent in Marketing Cloud Next. `check_mcn_compatibility` and `rewrite_for_mcn` flag these as **Not migratable** immediately.
245
261
 
262
+ ## MCN Handlebars
263
+
264
+ Marketing Cloud Next embeds a **locked-down Handlebars** templating layer (Handlebars.Net) alongside AMPscript. The server exposes the full helper catalog and conversion tooling, all data-driven from [`handlebars-data`](https://www.npmjs.com/package/handlebars-data) and the `handlebarsEquivalent` / `mcnHandlebarsGap` fields in [`ampscript-data`](https://www.npmjs.com/package/ampscript-data) — nothing is hand-maintained.
265
+
266
+ ### Authoring & lookup
267
+
268
+ ```
269
+ 1. list_handlebars_helpers → browse the catalog (filter by category / origin)
270
+ 2. lookup_handlebars_helper → signature, parameters, origin, MCN API version
271
+ 3. get_handlebars_completions → prefix-filtered completions
272
+ 4. writeHandlebars prompt → generate Handlebars-in-HTML, validated against the catalog
273
+ 5. validate_handlebars → helper names, arity, block balance, unsupported constructs
274
+ ```
275
+
276
+ `validate_handlebars` rejects constructs the MCN engine does not implement — partials (`{{> name}}`), decorators (`{{#* ...}}`), and built-in helpers absent from the locked-down runtime.
277
+
278
+ ### Conversion categories
279
+
280
+ AMPscript → Handlebars conversion classifies every function from `ampscript-data`:
281
+
282
+ | Category | Condition (in `ampscript-data`) | Output |
283
+ | -------- | ----------------------------------------------------- | ----------------------------------------------------------------- |
284
+ | **A** | `handlebarsEquivalent` is a helper name | Mapped to the canonical MCN Handlebars helper |
285
+ | **B** | No Handlebars counterpart | `{{!-- MANUAL_REWRITE_REQUIRED: … no Handlebars equivalent --}}` |
286
+ | **C** | `mcnHandlebarsGap: true` (MCN-supported, no helper yet) | `{{!-- MANUAL_REWRITE_REQUIRED: … (runtime gap) --}}` (distinct note) |
287
+
288
+ The Category C note is intentionally distinct from Category B so consumers can tell a *runtime gap* (function works in MCN AMPscript but has no Handlebars helper) apart from a function with no MCN counterpart at all. `convertSsjsToHandlebars` is transitive (SSJS → AMPscript → Handlebars) and conservatively flags imperative SSJS for manual rewrite.
289
+
246
290
  ## Writing effective prompts
247
291
 
248
292
  ### Automatic tool use
@@ -262,6 +306,8 @@ Clients that honour the MCP `instructions` field (Cursor, Claude Desktop, GitHub
262
306
  | Check if code is MCN-ready | Use `check_mcn_compatibility` with your file contents. |
263
307
  | Migrate code to Marketing Cloud Next | Use the `rewrite_for_mcn` **prompt** — it calls the tool internally for deterministic rewrites, then applies AI reasoning to any remaining manual sections. Use the tool directly only when you need structured JSON output in a pipeline. |
264
308
  | Convert SSJS ↔ AMPscript | Use the `convertSsjsToAmpscript` or `convertAmpscriptToSsjs` **prompt** — same hybrid pattern: tool runs first, AI handles what the rules couldn't. |
309
+ | Write or validate MCN Handlebars | Use the `writeHandlebars` prompt to author, or `validate_handlebars` to check existing template code. Look helpers up with `lookup_handlebars_helper` / `list_handlebars_helpers`. |
310
+ | Convert AMPscript ↔ Handlebars (or SSJS → Handlebars) | Use the `convertAmpscriptToHandlebars`, `convertHandlebarsToAmpscript`, or `convertSsjsToHandlebars` **prompt** — same hybrid pattern: tool runs first, AI handles what the rules couldn't. |
265
311
  | Review a code diff | Use the `reviewSfmcCode` prompt or mention "review the following diff". |
266
312
 
267
313
  ### Copy-paste prompt templates
@@ -34,6 +34,31 @@ export declare const PLATFORM_FUNCTION_TO_AMP: Readonly<Record<string, string>>;
34
34
  * PLATFORM_FUNCTION_TO_AMP at module load time.
35
35
  */
36
36
  export declare const AMP_TO_PLATFORM_FUNCTION: Readonly<Record<string, string>>;
37
+ /**
38
+ * Maps an AMPscript function name (lowercase) to the canonical MCN Handlebars
39
+ * helper name it converts to. Built at module load time from ampscript-data's
40
+ * `handlebarsEquivalent` field — always in sync with the installed
41
+ * ampscript-data version, never hand-edited (`mcp-conversion-rules-sync.mdc`).
42
+ *
43
+ * Only Category A functions (non-null string `handlebarsEquivalent`) are
44
+ * included. The stored value is the bare helper name; the canonical casing is
45
+ * resolved through `handlebars-data` so emitted `{{helper}}` calls match the
46
+ * catalog exactly.
47
+ */
48
+ export declare const AMP_TO_HANDLEBARS: Readonly<Record<string, string>>;
49
+ /**
50
+ * Maps a canonical MCN Handlebars helper name (lowercase) to its AMPscript
51
+ * function name. Inverted from AMP_TO_HANDLEBARS at module load time. Used by
52
+ * convertHandlebarsToAmpscript.
53
+ */
54
+ export declare const HANDLEBARS_TO_AMP: Readonly<Record<string, string>>;
55
+ /**
56
+ * Set of AMPscript function names (lowercase) flagged `mcnHandlebarsGap: true`
57
+ * in ampscript-data (Category C). These are documented as MCN-supported but
58
+ * currently fail at runtime and have no Handlebars helper — converting them
59
+ * must emit a MANUAL_REWRITE marker distinct from Category B (no counterpart).
60
+ */
61
+ export declare const AMP_MCN_HANDLEBARS_GAP: ReadonlySet<string>;
37
62
  /**
38
63
  * Maps an AMPscript function name (lowercase) to a native JavaScript rewrite
39
64
  * hint comment. Checked BEFORE the TreatAsContent polyfill fallback in
@@ -109,6 +134,53 @@ export declare function ampscriptToSsjs(code: string): ConversionResult;
109
134
  * @returns {string} Expression with `@` prefixes removed.
110
135
  */
111
136
  export declare function stripAmpVars(expr: string): string;
137
+ /**
138
+ * Distinct MANUAL_REWRITE note for Category C functions (`mcnHandlebarsGap`).
139
+ * Must be visibly different from the Category B note so consumers can tell a
140
+ * documented-but-broken function apart from one with no counterpart at all.
141
+ */
142
+ export declare const HBS_GAP_NOTE = "documented as supported in Marketing Cloud Next but currently fails at runtime \u2014 no Handlebars helper exists yet";
143
+ /**
144
+ * Convert AMPscript to MCN Handlebars using deterministic, data-driven rules.
145
+ *
146
+ * Inline expressions (`%%=Fn(args)=%%`, `%%=v(@x)=%%`, `%%var%%`) are mapped via
147
+ * the three conversion categories built from ampscript-data's
148
+ * `handlebarsEquivalent` / `mcnHandlebarsGap` fields:
149
+ * - **A** — mapped helper → `{{helper …}}`
150
+ * - **B** — no counterpart → `{{!-- MANUAL_REWRITE_REQUIRED … --}}`
151
+ * - **C** — `mcnHandlebarsGap` → `{{!-- MANUAL_REWRITE_REQUIRED … (distinct note) --}}`
152
+ *
153
+ * Procedural AMPscript blocks (`%%[ … ]%%`, `SET`/`VAR`/`IF`/`FOR`) have no
154
+ * Handlebars counterpart (Handlebars cannot assign variables or run imperative
155
+ * control flow) and are flagged MANUAL_REWRITE_REQUIRED.
156
+ * @param code - AMPscript source code (may include HTML context).
157
+ * @returns {ConversionResult} Converted Handlebars, change log, and flagged sections.
158
+ */
159
+ export declare function ampscriptToHandlebars(code: string): ConversionResult;
160
+ /**
161
+ * Convert MCN Handlebars to AMPscript using deterministic, data-driven rules.
162
+ *
163
+ * Inline helper calls (`{{helper args}}`) whose helper maps back to an AMPscript
164
+ * function (via HANDLEBARS_TO_AMP) become `%%=Fn(args)=%%`; bare variables
165
+ * (`{{name}}`) become `%%=v(@name)=%%`. Block helpers (`{{#each}}`…), partials
166
+ * (`{{> …}}`), dotted binding paths, and unknown helpers are flagged
167
+ * MANUAL_REWRITE_REQUIRED.
168
+ * @param code - Handlebars source code (may include HTML context).
169
+ * @returns {ConversionResult} Converted AMPscript, change log, and flagged sections.
170
+ */
171
+ export declare function handlebarsToAmpscript(code: string): ConversionResult;
172
+ /**
173
+ * Convert SSJS to MCN Handlebars deterministically via a two-step chain:
174
+ * SSJS → AMPscript (`ssjsToAmpscript`) → Handlebars (`ampscriptToHandlebars`).
175
+ *
176
+ * Because Handlebars is declarative, most imperative SSJS has no Handlebars
177
+ * counterpart and is conservatively flagged MANUAL_REWRITE_REQUIRED; inline
178
+ * `Platform.Function.X(…)` calls that map through AMPscript to a Handlebars
179
+ * helper are converted.
180
+ * @param code - SSJS source code (may include `<script runat="server">` tags).
181
+ * @returns {ConversionResult} Converted Handlebars, combined change log, and flagged sections.
182
+ */
183
+ export declare function ssjsToHandlebars(code: string): ConversionResult;
112
184
  export interface McnRewriteOptions {
113
185
  /** Function to check if an AMPscript function is MCN-supported */
114
186
  isMcnSupportedFn: (name: string) => boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"conversion-rules.d.ts","sourceRoot":"","sources":["../src/conversion-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,cAAc,EAAE,CAAC;CACrC;AAQD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMrE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKrE,CAAC;AAiBF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuBhE,CAAC;AAMF;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,EAAE,aAAa,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAG9E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAezD,CAAC;AAMH;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CASxD,CAAC;AAMH;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,EAAE,aAAa,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAmC3F,CAAC;AAMF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CA8K9D;AAsBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CA+J9D;AAwPD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAMD,MAAM,WAAW,iBAAiB;IAC9B,kEAAkE;IAClE,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C,0DAA0D;IAC1D,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,kBAAkB,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,UAAU,EAAE,OAAO,GAAG,OAAO,GAAG,aAAa,GAAG,gBAAgB,CAAC;IACjE,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAiL3F;AAMD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcjD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAQjE"}
1
+ {"version":3,"file":"conversion-rules.d.ts","sourceRoot":"","sources":["../src/conversion-rules.ts"],"names":[],"mappings":"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"}
@@ -37,6 +37,48 @@ export const AMP_TO_PLATFORM_FUNCTION = Object.fromEntries(PLATFORM_FUNCTIONS.fi
37
37
  */
38
38
  const SSJS_ONLY_FUNCTIONS = new Set(PLATFORM_FUNCTIONS.filter((f) => f.ampscriptEquivalent === null).map((f) => f.name.toLowerCase()));
39
39
  // ---------------------------------------------------------------------------
40
+ // Dynamic AMPscript ↔ MCN Handlebars maps (built from ampscript-data at load time)
41
+ // ---------------------------------------------------------------------------
42
+ import { FUNCTIONS as AMPSCRIPT_FUNCTIONS } from 'ampscript-data';
43
+ import { getHelper as getHandlebarsHelper } from 'handlebars-data';
44
+ /**
45
+ * Maps an AMPscript function name (lowercase) to the canonical MCN Handlebars
46
+ * helper name it converts to. Built at module load time from ampscript-data's
47
+ * `handlebarsEquivalent` field — always in sync with the installed
48
+ * ampscript-data version, never hand-edited (`mcp-conversion-rules-sync.mdc`).
49
+ *
50
+ * Only Category A functions (non-null string `handlebarsEquivalent`) are
51
+ * included. The stored value is the bare helper name; the canonical casing is
52
+ * resolved through `handlebars-data` so emitted `{{helper}}` calls match the
53
+ * catalog exactly.
54
+ */
55
+ export const AMP_TO_HANDLEBARS = Object.fromEntries(AMPSCRIPT_FUNCTIONS.filter((f) => typeof f.handlebarsEquivalent === 'string' && f.handlebarsEquivalent.length > 0).map((f) => {
56
+ const canonical = getHandlebarsHelper(f.handlebarsEquivalent);
57
+ return [
58
+ f.name.toLowerCase(),
59
+ canonical ? canonical.name : f.handlebarsEquivalent,
60
+ ];
61
+ }));
62
+ /**
63
+ * Maps a canonical MCN Handlebars helper name (lowercase) to its AMPscript
64
+ * function name. Inverted from AMP_TO_HANDLEBARS at module load time. Used by
65
+ * convertHandlebarsToAmpscript.
66
+ */
67
+ export const HANDLEBARS_TO_AMP = Object.fromEntries(AMPSCRIPT_FUNCTIONS.filter((f) => typeof f.handlebarsEquivalent === 'string' && f.handlebarsEquivalent.length > 0).map((f) => {
68
+ const canonical = getHandlebarsHelper(f.handlebarsEquivalent);
69
+ return [
70
+ (canonical ? canonical.name : f.handlebarsEquivalent).toLowerCase(),
71
+ f.name,
72
+ ];
73
+ }));
74
+ /**
75
+ * Set of AMPscript function names (lowercase) flagged `mcnHandlebarsGap: true`
76
+ * in ampscript-data (Category C). These are documented as MCN-supported but
77
+ * currently fail at runtime and have no Handlebars helper — converting them
78
+ * must emit a MANUAL_REWRITE marker distinct from Category B (no counterpart).
79
+ */
80
+ export const AMP_MCN_HANDLEBARS_GAP = new Set(AMPSCRIPT_FUNCTIONS.filter((f) => f.mcnHandlebarsGap === true).map((f) => f.name.toLowerCase()));
81
+ // ---------------------------------------------------------------------------
40
82
  // AMP_NATIVE_JS_HINTS: AMPscript-only functions with clean native JS rewrites
41
83
  // ---------------------------------------------------------------------------
42
84
  /**
@@ -698,6 +740,337 @@ function ampCondToSsjs(cond) {
698
740
  export function stripAmpVars(expr) {
699
741
  return expr.replaceAll(/@(\w+)/g, '$1');
700
742
  }
743
+ // ---------------------------------------------------------------------------
744
+ // AMPscript ↔ MCN Handlebars conversion
745
+ // ---------------------------------------------------------------------------
746
+ /**
747
+ * Distinct MANUAL_REWRITE note for Category C functions (`mcnHandlebarsGap`).
748
+ * Must be visibly different from the Category B note so consumers can tell a
749
+ * documented-but-broken function apart from one with no counterpart at all.
750
+ */
751
+ export const HBS_GAP_NOTE = 'documented as supported in Marketing Cloud Next but currently fails at runtime — no Handlebars helper exists yet';
752
+ /**
753
+ * Split a function-argument string into top-level comma-separated arguments,
754
+ * respecting nested parens/brackets and quoted strings. Returns an empty array
755
+ * for an empty/whitespace-only string.
756
+ * @param argsStr - The argument string (contents between the outer parens).
757
+ * @returns {string[]} Trimmed top-level arguments.
758
+ */
759
+ function splitArgs(argsStr) {
760
+ if (!argsStr.trim())
761
+ return [];
762
+ const args = [];
763
+ let depth = 0;
764
+ let current = '';
765
+ let quote = null;
766
+ for (const ch of argsStr) {
767
+ if (quote) {
768
+ current += ch;
769
+ if (ch === quote)
770
+ quote = null;
771
+ continue;
772
+ }
773
+ switch (ch) {
774
+ case '"':
775
+ case "'": {
776
+ quote = ch;
777
+ current += ch;
778
+ break;
779
+ }
780
+ case '(':
781
+ case '[': {
782
+ depth++;
783
+ current += ch;
784
+ break;
785
+ }
786
+ case ')':
787
+ case ']': {
788
+ depth--;
789
+ current += ch;
790
+ break;
791
+ }
792
+ default: {
793
+ if (ch === ',' && depth === 0) {
794
+ args.push(current.trim());
795
+ current = '';
796
+ }
797
+ else {
798
+ current += ch;
799
+ }
800
+ }
801
+ }
802
+ }
803
+ if (current.trim())
804
+ args.push(current.trim());
805
+ return args;
806
+ }
807
+ /**
808
+ * Convert a single AMPscript call argument to its Handlebars form: string and
809
+ * numeric literals pass through unchanged; `@var` references lose the `@`.
810
+ * @param arg - A single AMPscript argument.
811
+ * @returns {string} The Handlebars argument.
812
+ */
813
+ function ampArgToHbs(arg) {
814
+ const t = arg.trim();
815
+ if (/^["']/.test(t))
816
+ return t;
817
+ return stripAmpVars(t);
818
+ }
819
+ /**
820
+ * Convert a single Handlebars argument to its AMPscript form: string/number/
821
+ * boolean literals pass through; bare identifiers become `@var` references;
822
+ * dotted paths are returned unchanged (caller decides how to flag them).
823
+ * @param arg - A single Handlebars argument.
824
+ * @returns {string} The AMPscript argument.
825
+ */
826
+ function hbsArgToAmp(arg) {
827
+ const t = arg.trim();
828
+ if (/^["']/.test(t))
829
+ return t;
830
+ if (/^-?\d/.test(t))
831
+ return t;
832
+ if (/^(true|false|null)$/i.test(t))
833
+ return t;
834
+ if (t.startsWith('@'))
835
+ return t;
836
+ if (/^[A-Za-z_]\w*$/.test(t))
837
+ return `@${t}`;
838
+ return t;
839
+ }
840
+ /**
841
+ * Convert the inner text of a single AMPscript inline expression (`%%=…=%%`)
842
+ * to Handlebars, classifying function calls by the three conversion categories.
843
+ * @param inner - The expression between `%%=` and `=%%` (already trimmed).
844
+ * @param lineNum - Source line number for change/flag tracking.
845
+ * @param changes - Mutable change log.
846
+ * @param flaggedSections - Mutable flagged-section log.
847
+ * @returns {string} The Handlebars replacement string.
848
+ */
849
+ function convertInlineAmpToHbs(inner, lineNum, changes, flaggedSections) {
850
+ // v(@x) / v(x) → {{x}}
851
+ const vMatch = /^v\s*\(\s*@?([A-Za-z_]\w*)\s*\)$/i.exec(inner);
852
+ if (vMatch) {
853
+ changes.push({ line: lineNum, description: `%%=v(@${vMatch[1]})=%% → {{${vMatch[1]}}}` });
854
+ return `{{${vMatch[1]}}}`;
855
+ }
856
+ // Bare @var / var → {{var}}
857
+ const varMatch = /^@?([A-Za-z_]\w*)$/.exec(inner);
858
+ if (varMatch) {
859
+ changes.push({ line: lineNum, description: `%%=${inner}=%% → {{${varMatch[1]}}}` });
860
+ return `{{${varMatch[1]}}}`;
861
+ }
862
+ // FunctionName(args)
863
+ const fnMatch = /^([A-Za-z_]\w*)\s*\(([\s\S]*)\)$/.exec(inner);
864
+ if (fnMatch) {
865
+ const fnName = fnMatch[1];
866
+ const key = fnName.toLowerCase();
867
+ const hbsArgs = splitArgs(fnMatch[2]).map((a) => ampArgToHbs(a));
868
+ // Category A — mapped helper.
869
+ const helper = AMP_TO_HANDLEBARS[key];
870
+ if (helper) {
871
+ changes.push({
872
+ line: lineNum,
873
+ description: `%%=${fnName}(…)=%% → {{${helper} …}}`,
874
+ });
875
+ return `{{${helper}${hbsArgs.length > 0 ? ' ' + hbsArgs.join(' ') : ''}}}`;
876
+ }
877
+ // Category C — mcnHandlebarsGap (distinct note from Category B).
878
+ if (AMP_MCN_HANDLEBARS_GAP.has(key)) {
879
+ flaggedSections.push({
880
+ line: lineNum,
881
+ code: `%%=${inner}=%%`,
882
+ reason: `${fnName} is ${HBS_GAP_NOTE}`,
883
+ });
884
+ return `{{!-- MANUAL_REWRITE_REQUIRED: ${fnName} is ${HBS_GAP_NOTE} --}}`;
885
+ }
886
+ // Category B — no Handlebars counterpart.
887
+ flaggedSections.push({
888
+ line: lineNum,
889
+ code: `%%=${inner}=%%`,
890
+ reason: `AMPscript function '${fnName}' has no Handlebars equivalent`,
891
+ });
892
+ return `{{!-- MANUAL_REWRITE_REQUIRED: AMPscript function '${fnName}' has no Handlebars equivalent --}}`;
893
+ }
894
+ // Anything else (complex expression).
895
+ flaggedSections.push({
896
+ line: lineNum,
897
+ code: `%%=${inner}=%%`,
898
+ reason: 'complex AMPscript expression has no direct Handlebars form',
899
+ });
900
+ return `{{!-- MANUAL_REWRITE_REQUIRED: ${inner} --}}`;
901
+ }
902
+ /**
903
+ * Convert AMPscript to MCN Handlebars using deterministic, data-driven rules.
904
+ *
905
+ * Inline expressions (`%%=Fn(args)=%%`, `%%=v(@x)=%%`, `%%var%%`) are mapped via
906
+ * the three conversion categories built from ampscript-data's
907
+ * `handlebarsEquivalent` / `mcnHandlebarsGap` fields:
908
+ * - **A** — mapped helper → `{{helper …}}`
909
+ * - **B** — no counterpart → `{{!-- MANUAL_REWRITE_REQUIRED … --}}`
910
+ * - **C** — `mcnHandlebarsGap` → `{{!-- MANUAL_REWRITE_REQUIRED … (distinct note) --}}`
911
+ *
912
+ * Procedural AMPscript blocks (`%%[ … ]%%`, `SET`/`VAR`/`IF`/`FOR`) have no
913
+ * Handlebars counterpart (Handlebars cannot assign variables or run imperative
914
+ * control flow) and are flagged MANUAL_REWRITE_REQUIRED.
915
+ * @param code - AMPscript source code (may include HTML context).
916
+ * @returns {ConversionResult} Converted Handlebars, change log, and flagged sections.
917
+ */
918
+ export function ampscriptToHandlebars(code) {
919
+ const changes = [];
920
+ const flaggedSections = [];
921
+ const lines = code.split('\n');
922
+ const outputLines = [];
923
+ let inBlock = false;
924
+ for (const [i, line] of lines.entries()) {
925
+ const lineNum = i + 1;
926
+ const trimmed = line.trim();
927
+ // Track multi-line %%[ … ]%% blocks — flag the whole block once.
928
+ if (inBlock) {
929
+ if (/\]%%/.test(line))
930
+ inBlock = false;
931
+ continue;
932
+ }
933
+ if (/%%\[/.test(line)) {
934
+ if (!/\]%%/.test(line))
935
+ inBlock = true;
936
+ outputLines.push('{{!-- MANUAL_REWRITE_REQUIRED: AMPscript block (SET/VAR/IF/FOR) has no Handlebars equivalent — Handlebars cannot assign variables or run imperative control flow --}}');
937
+ flaggedSections.push({
938
+ line: lineNum,
939
+ code: trimmed.slice(0, 80),
940
+ reason: 'AMPscript procedural block has no Handlebars counterpart',
941
+ });
942
+ continue;
943
+ }
944
+ // Convert inline expressions on this line.
945
+ const converted = line
946
+ .replaceAll(/%%=\s*([\s\S]*?)\s*=%%/g, (_full, raw) => convertInlineAmpToHbs(raw.trim(), lineNum, changes, flaggedSections))
947
+ .replaceAll(/%%([A-Za-z_]\w*)%%/g, (_full, v) => {
948
+ changes.push({ line: lineNum, description: `%%${v}%% → {{${v}}}` });
949
+ return `{{${v}}}`;
950
+ });
951
+ outputLines.push(converted);
952
+ }
953
+ return { convertedCode: outputLines.join('\n'), changes, flaggedSections };
954
+ }
955
+ /**
956
+ * Convert MCN Handlebars to AMPscript using deterministic, data-driven rules.
957
+ *
958
+ * Inline helper calls (`{{helper args}}`) whose helper maps back to an AMPscript
959
+ * function (via HANDLEBARS_TO_AMP) become `%%=Fn(args)=%%`; bare variables
960
+ * (`{{name}}`) become `%%=v(@name)=%%`. Block helpers (`{{#each}}`…), partials
961
+ * (`{{> …}}`), dotted binding paths, and unknown helpers are flagged
962
+ * MANUAL_REWRITE_REQUIRED.
963
+ * @param code - Handlebars source code (may include HTML context).
964
+ * @returns {ConversionResult} Converted AMPscript, change log, and flagged sections.
965
+ */
966
+ export function handlebarsToAmpscript(code) {
967
+ const changes = [];
968
+ const flaggedSections = [];
969
+ let lineCursor = 1;
970
+ let lastIndex = 0;
971
+ const convertedCode = code.replaceAll(/\{\{([\s\S]*?)\}\}/g, (full, raw, offset) => {
972
+ lineCursor += countNewlines(code.slice(lastIndex, offset));
973
+ lastIndex = offset;
974
+ const lineNum = lineCursor;
975
+ const inner = raw.trim();
976
+ // Comments — preserve as an AMPscript comment.
977
+ if (inner.startsWith('!')) {
978
+ return full;
979
+ }
980
+ // Block helpers / partials / closing tags — no deterministic AMPscript form.
981
+ if (/^[#/>]/.test(inner)) {
982
+ flaggedSections.push({
983
+ line: lineNum,
984
+ code: full,
985
+ reason: 'Handlebars block helper / partial has no direct AMPscript equivalent',
986
+ });
987
+ return `%%-- MANUAL_REWRITE_REQUIRED: ${full} --%%`;
988
+ }
989
+ // Strip a leading no-escape ampersand: {{& x}}.
990
+ const body = inner.replace(/^&\s*/, '');
991
+ // Helper with arguments: {{helper arg1 arg2 …}}
992
+ const fnMatch = /^([A-Za-z_]\w*)\s+(.+)$/.exec(body);
993
+ if (fnMatch) {
994
+ const helperName = fnMatch[1];
995
+ const key = helperName.toLowerCase();
996
+ const ampName = HANDLEBARS_TO_AMP[key];
997
+ const ampArgs = splitArgs(fnMatch[2]).map((a) => hbsArgToAmp(a));
998
+ if (ampName) {
999
+ changes.push({
1000
+ line: lineNum,
1001
+ description: `{{${helperName} …}} → %%=${ampName}(…)=%%`,
1002
+ });
1003
+ return `%%=${ampName}(${ampArgs.join(', ')})=%%`;
1004
+ }
1005
+ flaggedSections.push({
1006
+ line: lineNum,
1007
+ code: full,
1008
+ reason: `Handlebars helper '${helperName}' has no AMPscript equivalent`,
1009
+ });
1010
+ return `%%-- MANUAL_REWRITE_REQUIRED: Handlebars helper '${helperName}' has no AMPscript equivalent --%%`;
1011
+ }
1012
+ // Bare single identifier: {{name}} → %%=v(@name)=%%
1013
+ const bareMatch = /^([A-Za-z_]\w*)$/.exec(body);
1014
+ if (bareMatch) {
1015
+ changes.push({
1016
+ line: lineNum,
1017
+ description: `{{${bareMatch[1]}}} → %%=v(@${bareMatch[1]})=%%`,
1018
+ });
1019
+ return `%%=v(@${bareMatch[1]})=%%`;
1020
+ }
1021
+ // Dotted binding path or other expression — context-specific.
1022
+ flaggedSections.push({
1023
+ line: lineNum,
1024
+ code: full,
1025
+ reason: 'Handlebars binding path requires context-specific AMPscript mapping',
1026
+ });
1027
+ return `%%-- MANUAL_REWRITE_REQUIRED: ${full} --%%`;
1028
+ });
1029
+ return { convertedCode, changes, flaggedSections };
1030
+ }
1031
+ /**
1032
+ * Count newline characters in a string (helper for line tracking in
1033
+ * handlebarsToAmpscript).
1034
+ * @param s - Input string.
1035
+ * @returns {number} Number of `\n` characters.
1036
+ */
1037
+ function countNewlines(s) {
1038
+ let n = 0;
1039
+ for (const ch of s) {
1040
+ if (ch === '\n')
1041
+ n++;
1042
+ }
1043
+ return n;
1044
+ }
1045
+ /**
1046
+ * Convert SSJS to MCN Handlebars deterministically via a two-step chain:
1047
+ * SSJS → AMPscript (`ssjsToAmpscript`) → Handlebars (`ampscriptToHandlebars`).
1048
+ *
1049
+ * Because Handlebars is declarative, most imperative SSJS has no Handlebars
1050
+ * counterpart and is conservatively flagged MANUAL_REWRITE_REQUIRED; inline
1051
+ * `Platform.Function.X(…)` calls that map through AMPscript to a Handlebars
1052
+ * helper are converted.
1053
+ * @param code - SSJS source code (may include `<script runat="server">` tags).
1054
+ * @returns {ConversionResult} Converted Handlebars, combined change log, and flagged sections.
1055
+ */
1056
+ export function ssjsToHandlebars(code) {
1057
+ const toAmp = ssjsToAmpscript(code);
1058
+ const toHbs = ampscriptToHandlebars(toAmp.convertedCode);
1059
+ return {
1060
+ convertedCode: toHbs.convertedCode,
1061
+ changes: [
1062
+ ...toAmp.changes.map((c) => ({
1063
+ line: c.line,
1064
+ description: `[SSJS→AMPscript] ${c.description}`,
1065
+ })),
1066
+ ...toHbs.changes.map((c) => ({
1067
+ line: c.line,
1068
+ description: `[AMPscript→Handlebars] ${c.description}`,
1069
+ })),
1070
+ ],
1071
+ flaggedSections: [...toAmp.flaggedSections, ...toHbs.flaggedSections],
1072
+ };
1073
+ }
701
1074
  /**
702
1075
  * Rewrite AMPscript code to be compatible with Marketing Cloud Next.
703
1076
  *