@emailens/mcp 0.7.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ MCP server for email compatibility analysis. Analyze, preview, diff, and fix ema
20
20
 
21
21
  Send **HTML, MJML, Maizzle or React Email**. Set `format` and the template is compiled before analysis, so what gets checked is the HTML your readers actually receive.
22
22
 
23
- Why your assistant needs this: across the 255 CSS and HTML features we track, only 6 are fully supported in every major email client ([see the data](https://emailens.dev/email-css/report)). Ask Claude to check your email before you send it.
23
+ Why your assistant needs this: across the 298 CSS and HTML features we track, only 6 are fully supported in every major email client ([see the data](https://emailens.dev/email-css/report)). Ask Claude to check your email before you send it.
24
24
 
25
25
  Built on [`@emailens/engine`](https://github.com/emailens/engine). Also available as a [GitHub Action](https://github.com/marketplace/actions/emailens-email-preview-check).
26
26
 
@@ -112,6 +112,7 @@ Quick CSS compatibility analysis; returns per-client scores and one finding per
112
112
  | `format` | enum | No | Input format |
113
113
  | `detail` | enum | No | `"summary"` (default) or `"full"` |
114
114
  | `clients` | string[] | No | Only report these client IDs |
115
+ | `targetingPolicy` | enum | No | `"progressive"` (default), `"strict"`, or `"lenient"` |
115
116
 
116
117
  **One finding per problem, not one per client.** The engine reports per client
117
118
  because a score is per client, and per selector because a fix is per selector.
@@ -171,7 +172,7 @@ authoritative.
171
172
 
172
173
  #### `audit_email`
173
174
 
174
- Comprehensive quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual bugs, dark-mode and mobile text contrast, and design consistency.
175
+ Comprehensive quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual bugs, dark-mode and mobile text contrast, design consistency, and client targeting.
175
176
 
176
177
  The last three cover what a light desktop preview cannot show: text that disappears when a client forces dark mode or when the email's own dark block repaints a surface without re-colouring the text on it, contrast below the email's breakpoint, and colours that differ by value but not to a reader.
177
178
 
@@ -182,6 +183,7 @@ The last three cover what a light desktop preview cannot show: text that disappe
182
183
  | `detail` | enum | No | `"summary"` (default) or `"full"` |
183
184
  | `clients` | string[] | No | Only report these client IDs |
184
185
  | `skip` | string[] | No | Checks to skip (e.g. `["spam", "images"]`) |
186
+ | `targetingPolicy` | enum | No | `"progressive"` (default), `"strict"`, or `"lenient"` |
185
187
 
186
188
  #### `fix_email`
187
189
 
@@ -247,8 +249,9 @@ Requires Dev plan ($9/mo). Share links expire after 7 days (Dev) or never (Pro).
247
249
  ## Template formats
248
250
 
249
251
  `format` accepts `html` (the default), `mjml`, `maizzle` and `jsx` (React
250
- Email). Anything but `html` is compiled before analysis and also decides the
251
- syntax the fix snippets come back in.
252
+ Email). `maizzle` is an HTML template or a Vue single-file component. A pasted
253
+ Vue file with no `format` is compiled as `maizzle`. Anything but `html` is
254
+ compiled before analysis and also decides the syntax the fix snippets come back in.
252
255
 
253
256
  Compiling matters more than it sounds. An email client renders the *output*, so
254
257
  that is what has to be checked: handed a raw `<mjml>` document, an HTML parser
@@ -261,7 +264,8 @@ dependencies:
261
264
 
262
265
  ```bash
263
266
  npm install mjml # MJML
264
- npm install @maizzle/framework # Maizzle
267
+ npm install @maizzle/framework@5 # Maizzle HTML
268
+ npm install @maizzle/framework@6 # Maizzle Vue single-file components
265
269
  npm install sucrase react @react-email/components @react-email/render # React Email
266
270
  ```
267
271
 
package/dist/index.js CHANGED
@@ -80,13 +80,21 @@ var COMPILED = /* @__PURE__ */ new Set(["jsx", "mjml", "maizzle"]);
80
80
  function isMissingCompiler(message) {
81
81
  return /requires (?:the )?"/.test(message);
82
82
  }
83
+ function isVueSfc(source) {
84
+ return /<template[\s>]/i.test(source) && /<\/template>/i.test(source);
85
+ }
86
+ function resolveSourceFormat(source, format) {
87
+ if (format) return format;
88
+ return isVueSfc(source) ? "maizzle" : "html";
89
+ }
83
90
  async function toHtml(source, format) {
84
- if (!format || format === "html") return { ok: true, html: source };
85
- if (!COMPILED.has(format)) {
86
- return { ok: false, message: `Unknown format "${format}". Use html, jsx, mjml, or maizzle.` };
91
+ const resolved = resolveSourceFormat(source, format);
92
+ if (resolved === "html") return { ok: true, html: source };
93
+ if (!COMPILED.has(resolved)) {
94
+ return { ok: false, message: `Unknown format "${resolved}". Use html, jsx, mjml, or maizzle.` };
87
95
  }
88
96
  try {
89
- return { ok: true, html: await compile(source, format) };
97
+ return { ok: true, html: await compile(source, resolved) };
90
98
  } catch (err) {
91
99
  const message = err instanceof Error ? err.message : String(err);
92
100
  if (isMissingCompiler(message)) {
@@ -106,9 +114,11 @@ import {
106
114
  createSession,
107
115
  analyzeEmail,
108
116
  generateCompatibilityScore,
117
+ simulateDarkMode,
118
+ transformForAllClients,
119
+ transformForClient,
109
120
  generateFixPrompt,
110
121
  estimateAiFixTokens,
111
- structuralWarnings,
112
122
  diffResults,
113
123
  toPlainText,
114
124
  EMAIL_CLIENTS as EMAIL_CLIENTS2,
@@ -209,10 +219,36 @@ function toFramework(format) {
209
219
  if (format === "jsx" || format === "mjml" || format === "maizzle") return format;
210
220
  return void 0;
211
221
  }
222
+ function worstScore(scores) {
223
+ const values = Object.values(scores);
224
+ if (values.length === 0) return 0;
225
+ return Math.min(...values.map((s) => s.score));
226
+ }
212
227
  var formatEnum = z.enum(["html", "jsx", "mjml", "maizzle"]).optional();
213
228
  var detailEnum = z.enum(["summary", "full"]).optional().describe(
214
229
  "'summary' (default) returns one entry per problem, listing the clients it affects (roughly 10x smaller). 'full' returns one entry per client with fix snippets."
215
230
  );
231
+ var targetingPolicyEnum = z.enum(["progressive", "strict", "lenient"]).optional().describe(
232
+ "'progressive' (default) suppresses compatibility false-positives inside client-targeted scopes and only flags deprecated or dangerous hacks. 'strict' treats targeting as non-standard and reports every hack. 'lenient' suppresses false positives and ignores non-fatal targeting warnings."
233
+ );
234
+ var skipEnum = z.enum([
235
+ "spam",
236
+ "links",
237
+ "accessibility",
238
+ "images",
239
+ "compatibility",
240
+ "inboxPreview",
241
+ "size",
242
+ "templateVariables",
243
+ "overflow",
244
+ "visual",
245
+ "darkContrast",
246
+ "mobileContrast",
247
+ "design",
248
+ "vml",
249
+ "styleSurvival",
250
+ "targeting"
251
+ ]);
216
252
  var clientsParam = z.array(z.string()).optional().describe(
217
253
  "Only report these client IDs (e.g. ['gmail-web','outlook-windows']). The fastest way to cut the response when you care about specific clients. Use list_clients for the IDs."
218
254
  );
@@ -270,7 +306,7 @@ function validateHtmlSize(html) {
270
306
  }
271
307
  var server = new McpServer({
272
308
  name: "emailens",
273
- version: "0.7.0"
309
+ version: "0.9.1"
274
310
  });
275
311
  server.registerTool(
276
312
  "preview_email",
@@ -280,7 +316,8 @@ server.registerTool(
280
316
  inputSchema: {
281
317
  html: z.string().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so"),
282
318
  clients: z.array(z.string()).optional().describe("Optional client ID filter (e.g. ['gmail-web', 'outlook-windows'])"),
283
- format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets.")
319
+ format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
320
+ targetingPolicy: targetingPolicyEnum
284
321
  },
285
322
  annotations: {
286
323
  title: "Preview Email",
@@ -290,14 +327,14 @@ server.registerTool(
290
327
  openWorldHint: false
291
328
  }
292
329
  },
293
- async ({ html, clients, format }) => {
330
+ async ({ html, clients, format, targetingPolicy }) => {
294
331
  const sizeError = validateHtmlSize(html);
295
332
  if (sizeError) return sizeError;
296
333
  const source = await toHtml(html, format);
297
334
  if (!source.ok) return mcpError(source.message);
298
335
  const validClientIds = new Set(EMAIL_CLIENTS2.map((c) => c.id));
299
- const framework = toFramework(format);
300
- const session = createSession(source.html, { framework });
336
+ const framework = toFramework(resolveSourceFormat(html, format));
337
+ const session = createSession(source.html, { framework, targetingPolicy });
301
338
  let transforms;
302
339
  if (clients) {
303
340
  const filter = clients.filter((c) => validClientIds.has(c));
@@ -306,21 +343,20 @@ server.registerTool(
306
343
  JSON.stringify({ error: "No valid client IDs provided", validClientIds: Array.from(validClientIds) }, null, 2)
307
344
  );
308
345
  }
309
- transforms = filter.map((c) => session.transformForClient(c));
346
+ transforms = filter.map((c) => transformForClient(source.html, c, framework));
310
347
  } else {
311
- transforms = session.transformForAllClients();
348
+ transforms = transformForAllClients(source.html, framework);
312
349
  }
313
350
  const warnings = session.analyze();
314
- const scores = session.score(warnings);
351
+ const scores = generateCompatibilityScore(warnings);
315
352
  const inboxPreview = session.extractInboxPreview();
316
353
  const sizeReport = session.checkSize();
317
354
  const darkMode = {};
318
355
  for (const t of transforms) {
319
- darkMode[t.clientId] = session.simulateDarkMode(t.clientId);
356
+ darkMode[t.clientId] = simulateDarkMode(source.html, t.clientId);
320
357
  }
321
- const scoreValues = Object.values(scores);
322
- const overallScore = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
323
- const plainText = toPlainText(html);
358
+ const overallScore = worstScore(scores);
359
+ const plainText = toPlainText(source.html);
324
360
  const result = {
325
361
  overallScore,
326
362
  compatibilityScores: scores,
@@ -358,9 +394,10 @@ server.registerTool(
358
394
  description: "Quick CSS compatibility analysis, returns per-client scores and one finding per problem, listing the clients it affects. For HTML input each finding carries loc (line, column, offset) and alsoAtLines, so you can edit the exact source. Narrow with clients, or pass detail:'full' for the per-client breakdown with fix snippets. Use audit_email for a full quality report (spam, links, a11y, images, etc.).",
359
395
  inputSchema: {
360
396
  html: z.string().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so"),
361
- format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets."),
397
+ format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
362
398
  detail: detailEnum,
363
- clients: clientsParam
399
+ clients: clientsParam,
400
+ targetingPolicy: targetingPolicyEnum
364
401
  },
365
402
  annotations: {
366
403
  title: "Analyze Email",
@@ -370,19 +407,19 @@ server.registerTool(
370
407
  openWorldHint: false
371
408
  }
372
409
  },
373
- async ({ html, format, detail, clients }) => {
410
+ async ({ html, format, detail, clients, targetingPolicy }) => {
374
411
  const sizeError = validateHtmlSize(html);
375
412
  if (sizeError) return sizeError;
376
413
  const clientError = validateClients(clients);
377
414
  if (clientError) return clientError;
378
415
  const source = await toHtml(html, format);
379
416
  if (!source.ok) return mcpError(source.message);
380
- const warnings = analyzeEmail(source.html, toFramework(format), {
381
- positions: positionsApply(format)
417
+ const warnings = analyzeEmail(source.html, toFramework(resolveSourceFormat(html, format)), {
418
+ positions: positionsApply(resolveSourceFormat(html, format)),
419
+ targetingPolicy
382
420
  });
383
421
  const scores = generateCompatibilityScore(warnings);
384
- const scoreValues = Object.values(scores);
385
- const overallScore = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
422
+ const overallScore = worstScore(scores);
386
423
  return {
387
424
  content: [
388
425
  {
@@ -405,13 +442,14 @@ server.registerTool(
405
442
  "audit_email",
406
443
  {
407
444
  title: "Audit Email",
408
- description: "Comprehensive email quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual fallbacks, dark-mode and mobile text contrast, design consistency, and structural faults in Outlook-only VML (which lives inside conditional comments and is invisible to every other check). For HTML input, findings tied to a specific element carry loc (line, column, offset) so you can edit the exact source. Compatibility is collapsed to one finding per problem listing the clients it affects; pass detail:'full' for the per-client breakdown with fix snippets. Use skip to omit checks and clients to narrow which clients are reported.",
445
+ description: "Comprehensive email quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual fallbacks, dark-mode and mobile text contrast, design consistency, structural faults in Outlook-only VML (which lives inside conditional comments and is invisible to every other check), style survival: CSS a client parses correctly and then discards, such as a stylesheet past Gmail's 16 KB ceiling or every rule after a `}}`, which no support matrix can express, and client targeting: HowToTarget hacks such as Gmail u+.body or MSO conditionals, which do not score as css-hack. For HTML input, findings tied to a specific element carry loc (line, column, offset) so you can edit the exact source. Compatibility is collapsed to one finding per problem listing the clients it affects; pass detail:'full' for the per-client breakdown with fix snippets. Use skip to omit checks, targetingPolicy to change how targeted scopes affect scores, and clients to narrow which clients are reported.",
409
446
  inputSchema: {
410
447
  html: z.string().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so"),
411
- format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets."),
412
- skip: z.array(z.enum(["spam", "links", "accessibility", "images", "compatibility", "inboxPreview", "size", "templateVariables", "overflow", "visual", "darkContrast", "mobileContrast", "design", "vml"])).optional().describe("Checks to skip (e.g. ['spam', 'images'])"),
448
+ format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
449
+ skip: z.array(skipEnum).optional().describe("Checks to skip (e.g. ['spam', 'images'])"),
413
450
  detail: detailEnum,
414
- clients: clientsParam
451
+ clients: clientsParam,
452
+ targetingPolicy: targetingPolicyEnum
415
453
  },
416
454
  annotations: {
417
455
  title: "Audit Email",
@@ -421,7 +459,7 @@ server.registerTool(
421
459
  openWorldHint: false
422
460
  }
423
461
  },
424
- async ({ html, format, skip, detail, clients }) => {
462
+ async ({ html, format, skip, detail, clients, targetingPolicy }) => {
425
463
  const sizeError = validateHtmlSize(html);
426
464
  if (sizeError) return sizeError;
427
465
  const clientError = validateClients(clients);
@@ -429,12 +467,12 @@ server.registerTool(
429
467
  const source = await toHtml(html, format);
430
468
  if (!source.ok) return mcpError(source.message);
431
469
  const session = createSession(source.html, {
432
- framework: toFramework(format),
433
- positions: positionsApply(format)
470
+ framework: toFramework(resolveSourceFormat(html, format)),
471
+ positions: positionsApply(resolveSourceFormat(html, format)),
472
+ targetingPolicy
434
473
  });
435
474
  const report = session.audit({ skip });
436
- const scoreValues = Object.values(report.compatibility.scores);
437
- const overallCompatibility = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
475
+ const overallCompatibility = worstScore(report.compatibility.scores);
438
476
  return {
439
477
  content: [
440
478
  {
@@ -458,7 +496,9 @@ server.registerTool(
458
496
  vml: report.vml,
459
497
  darkContrast: report.darkContrast,
460
498
  mobileContrast: report.mobileContrast,
461
- design: report.design
499
+ design: report.design,
500
+ styleSurvival: report.styleSurvival,
501
+ targeting: report.targeting
462
502
  },
463
503
  null,
464
504
  2
@@ -475,7 +515,7 @@ server.registerTool(
475
515
  description: "Generate a structured fix prompt for email compatibility issues. Returns markdown with the original code, detected issues (CSS or structural), fix snippets, and format-specific instructions. Use after preview_email or analyze_email.",
476
516
  inputSchema: {
477
517
  html: z.string().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so: the source to fix"),
478
- format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets."),
518
+ format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
479
519
  scope: z.enum(["all", "current"]).optional().describe("'all' (default) or 'current' (requires selectedClientId)"),
480
520
  selectedClientId: z.string().optional().describe("Client ID to scope fixes to (e.g. 'outlook-windows')")
481
521
  },
@@ -492,9 +532,10 @@ server.registerTool(
492
532
  if (sizeError) return sizeError;
493
533
  const source = await toHtml(html, format);
494
534
  if (!source.ok) return mcpError(source.message);
495
- const framework = toFramework(format);
535
+ const resolved = resolveSourceFormat(html, format);
536
+ const framework = toFramework(resolved);
496
537
  const fixScope = scope === "current" ? "current" : "all";
497
- const inputFormat = format || "html";
538
+ const inputFormat = resolved;
498
539
  const warnings = analyzeEmail(source.html, framework);
499
540
  const scores = generateCompatibilityScore(warnings);
500
541
  if (warnings.length === 0) {
@@ -523,7 +564,7 @@ server.registerTool(
523
564
  selectedClientId,
524
565
  format: inputFormat
525
566
  });
526
- const structural = structuralWarnings(warnings);
567
+ const structural = warnings.filter((w) => w.fixType === "structural");
527
568
  return {
528
569
  content: [
529
570
  {
@@ -592,7 +633,7 @@ server.registerTool(
592
633
  inputSchema: {
593
634
  before: z.string().describe("Original email source"),
594
635
  after: z.string().describe("Modified email source, in the same format"),
595
- format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets.")
636
+ format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.")
596
637
  },
597
638
  annotations: {
598
639
  title: "Diff Emails",
@@ -611,7 +652,9 @@ server.registerTool(
611
652
  if (!beforeSource.ok) return mcpError(beforeSource.message);
612
653
  const afterSource = await toHtml(after, format);
613
654
  if (!afterSource.ok) return mcpError(afterSource.message);
614
- const framework = toFramework(format);
655
+ const framework = toFramework(
656
+ resolveSourceFormat(before, format) === "maizzle" || resolveSourceFormat(after, format) === "maizzle" ? format ?? "maizzle" : format ?? "html"
657
+ );
615
658
  const beforeWarnings = analyzeEmail(beforeSource.html, framework);
616
659
  const beforeScores = generateCompatibilityScore(beforeWarnings);
617
660
  const afterWarnings = analyzeEmail(afterSource.html, framework);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/findings.ts","../src/compile.ts","../src/config.ts","../src/hosted.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\r\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\r\nimport { z } from \"zod\";\r\nimport { collapseWarnings, forClients, knownClientIds } from \"./findings.js\";\r\nimport { toHtml } from \"./compile.js\";\r\nimport {\r\n createSession,\r\n analyzeEmail,\r\n generateCompatibilityScore,\r\n generateFixPrompt,\r\n estimateAiFixTokens,\r\n structuralWarnings,\r\n diffResults,\r\n toPlainText,\r\n EMAIL_CLIENTS,\r\n MAX_HTML_SIZE,\r\n type Framework,\r\n type CSSWarning,\r\n type SourceLocation,\r\n} from \"@emailens/engine\";\r\n// Lazy-imported: @emailens/engine/server may not exist in all engine versions\r\nlet _checkDeliverability: ((domain: string) => Promise<unknown>) | null = null;\r\nimport { config } from \"./config.js\";\r\nimport { apiRequest, mcpError, noApiKeyError } from \"./hosted.js\";\r\n\r\nfunction toFramework(format?: string): Framework | undefined {\r\n if (format === \"jsx\" || format === \"mjml\" || format === \"maizzle\") return format;\r\n return undefined;\r\n}\r\n\r\n/**\r\n * The language the source is written in.\r\n *\r\n * Anything but `html` is compiled before analysis and also selects the syntax\r\n * the fix snippets are written in.\r\n */\r\nconst formatEnum = z.enum([\"html\", \"jsx\", \"mjml\", \"maizzle\"]).optional();\r\n\r\n/**\r\n * How much compatibility detail to return.\r\n *\r\n * `summary` collapses the engine's per-client, per-selector warnings into one\r\n * entry per problem listing the clients it affects: the same information at a\r\n * fraction of the tokens. `full` is the engine's own shape, per client, with\r\n * fix snippets, for a caller that wants everything.\r\n */\r\nconst detailEnum = z\r\n .enum([\"summary\", \"full\"])\r\n .optional()\r\n .describe(\r\n \"'summary' (default) returns one entry per problem, listing the clients it affects (roughly 10x smaller). 'full' returns one entry per client with fix snippets.\",\r\n );\r\n\r\nconst clientsParam = z\r\n .array(z.string())\r\n .optional()\r\n .describe(\r\n \"Only report these client IDs (e.g. ['gmail-web','outlook-windows']). The fastest way to cut the response when you care about specific clients. Use list_clients for the IDs.\",\r\n );\r\n\r\n/**\r\n * Reject a client id the engine does not know.\r\n *\r\n * Filtering to a typo would return an empty list, which reads as \"this email\r\n * is fine for that client\"; the most misleading answer the tool can give.\r\n */\r\nfunction validateClients(clients: string[] | undefined) {\r\n if (!clients?.length) return undefined;\r\n const known = new Set(knownClientIds());\r\n const unknown = clients.filter((c) => !known.has(c));\r\n if (!unknown.length) return undefined;\r\n return mcpError(\r\n `Unknown client ${unknown.length > 1 ? \"IDs\" : \"ID\"}: ${unknown.join(\", \")}. Call list_clients for the valid IDs.`,\r\n );\r\n}\r\n\r\n/** The compatibility payload, at the requested level of detail. */\r\nfunction compatibilityPayload(\r\n warnings: CSSWarning[],\r\n detail: \"summary\" | \"full\" | undefined,\r\n clients: string[] | undefined,\r\n) {\r\n const scoped = forClients(warnings, clients);\r\n if (detail === \"full\") {\r\n return {\r\n warningCount: scoped.length,\r\n warnings: scoped.map((w) => ({\r\n client: w.client,\r\n property: w.property,\r\n severity: w.severity,\r\n message: w.message,\r\n suggestion: w.suggestion,\r\n fix: w.fix,\r\n fixType: w.fixType,\r\n ...withPositions(w),\r\n })),\r\n };\r\n }\r\n const findings = collapseWarnings(scoped);\r\n return {\r\n warningCount: scoped.length,\r\n findingCount: findings.length,\r\n findings,\r\n };\r\n}\r\n\r\n/**\r\n * Do source positions refer to the code the caller handed us?\r\n *\r\n * Only for plain HTML. JSX, MJML and Maizzle are compiled before analysis, so a\r\n * position would point into generated output: worse than none, because an\r\n * agent would edit the wrong line with confidence.\r\n */\r\nfunction positionsApply(format?: string): boolean {\r\n return (format ?? \"html\") === \"html\";\r\n}\r\n\r\n/**\r\n * The position fields a finding carries, when it has them.\r\n *\r\n * Deliberately compact: this response is read by a model paying for every\r\n * token, and a newsletter can produce a thousand occurrences. The first one\r\n * gets a full position, enough to edit it, and the rest are reduced to line\r\n * numbers, which is all an assistant needs to go find them. Carrying every\r\n * occurrence in full nearly doubled the payload on a real fixture.\r\n */\r\nfunction withPositions<T extends {\r\n loc?: SourceLocation;\r\n locs?: SourceLocation[];\r\n locsTruncated?: boolean;\r\n}>(w: T) {\r\n if (!w.loc) return {};\r\n const { line, column, offset, length } = w.loc;\r\n const others = (w.locs ?? []).slice(1).map((l) => l.line);\r\n return {\r\n loc: { line, column, offset, length },\r\n ...(others.length ? { alsoAtLines: others } : {}),\r\n ...(w.locsTruncated ? { locsTruncated: true } : {}),\r\n };\r\n}\r\n\r\nfunction validateHtmlSize(html: string) {\r\n if (html.length > MAX_HTML_SIZE) {\r\n return mcpError(`HTML input exceeds ${Math.round(MAX_HTML_SIZE / 1024)}KB limit. Reduce the email size and try again.`);\r\n }\r\n return null;\r\n}\r\n\r\nconst server = new McpServer({\r\n name: \"emailens\",\r\n version: \"0.7.0\",\r\n});\r\n\r\n// ── Local Tool: preview_email ──────────────────────────────────────\r\n\r\nserver.registerTool(\r\n \"preview_email\",\r\n {\r\n title: \"Preview Email\",\r\n description:\r\n `Full email compatibility preview, transforms HTML for ${EMAIL_CLIENTS.length} email clients (Gmail, Outlook, Apple Mail, Yahoo, Samsung, Thunderbird, HEY, Proton Mail, AOL, Fastmail, Superhuman), analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.`,\r\n inputSchema: {\r\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\r\n clients: z\r\n .array(z.string())\r\n .optional()\r\n .describe(\"Optional client ID filter (e.g. ['gmail-web', 'outlook-windows'])\"),\r\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets.\"),\r\n },\r\n annotations: {\r\n title: \"Preview Email\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async ({ html, clients, format }) => {\r\n const sizeError = validateHtmlSize(html);\r\n if (sizeError) return sizeError;\r\n\r\n const source = await toHtml(html, format);\r\n if (!source.ok) return mcpError(source.message);\r\n\r\n const validClientIds = new Set(EMAIL_CLIENTS.map((c) => c.id));\r\n const framework = toFramework(format);\r\n const session = createSession(source.html, { framework });\r\n\r\n let transforms;\r\n if (clients) {\r\n const filter = clients.filter((c) => validClientIds.has(c));\r\n if (filter.length === 0) {\r\n return mcpError(\r\n JSON.stringify({ error: \"No valid client IDs provided\", validClientIds: Array.from(validClientIds) }, null, 2),\r\n );\r\n }\r\n transforms = filter.map((c) => session.transformForClient(c));\r\n } else {\r\n transforms = session.transformForAllClients();\r\n }\r\n\r\n const warnings = session.analyze();\r\n const scores = session.score(warnings);\r\n const inboxPreview = session.extractInboxPreview();\r\n const sizeReport = session.checkSize();\r\n\r\n const darkMode: Record<string, { html: string; warnings: CSSWarning[] }> = {};\r\n for (const t of transforms) {\r\n darkMode[t.clientId] = session.simulateDarkMode(t.clientId);\r\n }\r\n\r\n const scoreValues = Object.values(scores);\r\n const overallScore =\r\n scoreValues.length > 0\r\n ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length)\r\n : 0;\r\n\r\n const plainText = toPlainText(html);\r\n\r\n const result: Record<string, unknown> = {\r\n overallScore,\r\n compatibilityScores: scores,\r\n cssWarnings: warnings.map((w) => ({\r\n client: w.client,\r\n property: w.property,\r\n severity: w.severity,\r\n message: w.message,\r\n suggestion: w.suggestion,\r\n fix: w.fix,\r\n fixType: w.fixType,\r\n })),\r\n inboxPreview,\r\n sizeReport,\r\n ...(plainText ? { plainText } : {}),\r\n clientCount: transforms.length,\r\n darkModeWarnings: Object.entries(darkMode).reduce(\r\n (acc, [clientId, dm]) => {\r\n if (dm.warnings.length > 0) acc[clientId] = dm.warnings.length;\r\n return acc;\r\n },\r\n {} as Record<string, number>,\r\n ),\r\n };\r\n\r\n // Contextual tip: only when no API key and score indicates issues\r\n if (!config.isHosted && overallScore < 90) {\r\n result.tip =\r\n \"Capture real screenshots to verify these issues across clients. Add EMAILENS_API_KEY to your MCP config \\u2192 emailens.dev/settings/api-keys?ref=mcp\";\r\n }\r\n\r\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] };\r\n },\r\n);\r\n\r\n// ── Local Tool: analyze_email (CSS-only, reverted from audit) ──────\r\n\r\nserver.registerTool(\r\n \"analyze_email\",\r\n {\r\n title: \"Analyze Email\",\r\n description:\r\n \"Quick CSS compatibility analysis, returns per-client scores and one finding per problem, listing the clients it affects. For HTML input each finding carries loc (line, column, offset) and alsoAtLines, so you can edit the exact source. Narrow with clients, or pass detail:'full' for the per-client breakdown with fix snippets. Use audit_email for a full quality report (spam, links, a11y, images, etc.).\",\r\n inputSchema: {\r\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\r\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets.\"),\r\n detail: detailEnum,\r\n clients: clientsParam,\r\n },\r\n annotations: {\r\n title: \"Analyze Email\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async ({ html, format, detail, clients }) => {\r\n const sizeError = validateHtmlSize(html);\r\n if (sizeError) return sizeError;\r\n const clientError = validateClients(clients);\r\n if (clientError) return clientError;\r\n\r\n const source = await toHtml(html, format);\r\n if (!source.ok) return mcpError(source.message);\r\n\r\n const warnings = analyzeEmail(source.html, toFramework(format), {\r\n positions: positionsApply(format),\r\n });\r\n // Scores stay whole-email: filtering to two clients should narrow what is\r\n // reported, not silently change what the email scores.\r\n const scores = generateCompatibilityScore(warnings);\r\n\r\n const scoreValues = Object.values(scores);\r\n const overallScore =\r\n scoreValues.length > 0\r\n ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length)\r\n : 0;\r\n\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n overallScore,\r\n scores,\r\n ...compatibilityPayload(warnings, detail, clients),\r\n },\r\n null,\r\n 2,\r\n ),\r\n },\r\n ],\r\n };\r\n },\r\n);\r\n\r\n// ── Local Tool: audit_email ────────────────────────────────────────\r\n\r\nserver.registerTool(\r\n \"audit_email\",\r\n {\r\n title: \"Audit Email\",\r\n description:\r\n \"Comprehensive email quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual fallbacks, dark-mode and mobile text contrast, design consistency, and structural faults in Outlook-only VML (which lives inside conditional comments and is invisible to every other check). For HTML input, findings tied to a specific element carry loc (line, column, offset) so you can edit the exact source. Compatibility is collapsed to one finding per problem listing the clients it affects; pass detail:'full' for the per-client breakdown with fix snippets. Use skip to omit checks and clients to narrow which clients are reported.\",\r\n inputSchema: {\r\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\r\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets.\"),\r\n skip: z\r\n .array(z.enum([\"spam\", \"links\", \"accessibility\", \"images\", \"compatibility\", \"inboxPreview\", \"size\", \"templateVariables\", \"overflow\", \"visual\", \"darkContrast\", \"mobileContrast\", \"design\", \"vml\"]))\r\n .optional()\r\n .describe(\"Checks to skip (e.g. ['spam', 'images'])\"),\r\n detail: detailEnum,\r\n clients: clientsParam,\r\n },\r\n annotations: {\r\n title: \"Audit Email\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async ({ html, format, skip, detail, clients }) => {\r\n const sizeError = validateHtmlSize(html);\r\n if (sizeError) return sizeError;\r\n const clientError = validateClients(clients);\r\n if (clientError) return clientError;\r\n\r\n const source = await toHtml(html, format);\r\n if (!source.ok) return mcpError(source.message);\r\n\r\n const session = createSession(source.html, {\r\n framework: toFramework(format),\r\n positions: positionsApply(format),\r\n });\r\n const report = session.audit({ skip });\r\n\r\n const scoreValues = Object.values(report.compatibility.scores);\r\n const overallCompatibility =\r\n scoreValues.length > 0\r\n ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length)\r\n : 0;\r\n\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n overallCompatibility,\r\n compatibility: {\r\n scores: report.compatibility.scores,\r\n ...compatibilityPayload(report.compatibility.warnings, detail, clients),\r\n },\r\n spam: report.spam,\r\n links: report.links,\r\n accessibility: report.accessibility,\r\n images: report.images,\r\n inboxPreview: report.inboxPreview,\r\n size: report.size,\r\n templateVariables: report.templateVariables,\r\n overflow: report.overflow,\r\n visual: report.visual,\r\n vml: report.vml,\r\n darkContrast: report.darkContrast,\r\n mobileContrast: report.mobileContrast,\r\n design: report.design,\r\n },\r\n null,\r\n 2,\r\n ),\r\n },\r\n ],\r\n };\r\n },\r\n);\r\n\r\n// ── Local Tool: fix_email ──────────────────────────────────────────\r\n\r\nserver.registerTool(\r\n \"fix_email\",\r\n {\r\n title: \"Fix Email\",\r\n description:\r\n \"Generate a structured fix prompt for email compatibility issues. Returns markdown with the original code, detected issues (CSS or structural), fix snippets, and format-specific instructions. Use after preview_email or analyze_email.\",\r\n inputSchema: {\r\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so: the source to fix\"),\r\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets.\"),\r\n scope: z.enum([\"all\", \"current\"]).optional().describe(\"'all' (default) or 'current' (requires selectedClientId)\"),\r\n selectedClientId: z.string().optional().describe(\"Client ID to scope fixes to (e.g. 'outlook-windows')\"),\r\n },\r\n annotations: {\r\n title: \"Fix Email\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async ({ html, format, scope, selectedClientId }) => {\r\n const sizeError = validateHtmlSize(html);\r\n if (sizeError) return sizeError;\r\n\r\n // The findings come from the compiled output, because that is what a\r\n // client renders. The prompt keeps the caller's own source, because that is\r\n // the file they will edit; a fix written against generated HTML is not a\r\n // fix they can apply.\r\n const source = await toHtml(html, format);\r\n if (!source.ok) return mcpError(source.message);\r\n\r\n const framework = toFramework(format);\r\n const fixScope = scope === \"current\" ? \"current\" : \"all\";\r\n const inputFormat = format || \"html\";\r\n\r\n const warnings = analyzeEmail(source.html, framework);\r\n const scores = generateCompatibilityScore(warnings);\r\n\r\n if (warnings.length === 0) {\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify({ message: \"No compatibility issues found \\u2014 nothing to fix.\", overallScore: 100 }, null, 2),\r\n },\r\n ],\r\n };\r\n }\r\n\r\n const estimate = await estimateAiFixTokens({\r\n originalHtml: html,\r\n warnings,\r\n scores,\r\n scope: fixScope,\r\n selectedClientId,\r\n format: inputFormat,\r\n });\r\n\r\n const prompt = generateFixPrompt({\r\n originalHtml: html,\r\n warnings,\r\n scores,\r\n scope: fixScope,\r\n selectedClientId,\r\n format: inputFormat,\r\n });\r\n\r\n const structural = structuralWarnings(warnings);\r\n\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n totalWarnings: warnings.length,\r\n structuralWarnings: structural.length,\r\n cssWarnings: warnings.length - structural.length,\r\n tokenEstimate: {\r\n inputTokens: estimate.inputTokens,\r\n estimatedOutputTokens: estimate.estimatedOutputTokens,\r\n truncated: estimate.truncated,\r\n },\r\n note: \"Apply the fixes in the prompt below. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes.\",\r\n },\r\n null,\r\n 2,\r\n ),\r\n },\r\n { type: \"text\" as const, text: prompt },\r\n ],\r\n };\r\n },\r\n);\r\n\r\n// ── Local Tool: list_clients ───────────────────────────────────────\r\n\r\nserver.registerTool(\r\n \"list_clients\",\r\n {\r\n title: \"List Email Clients\",\r\n description: \"List every supported email client with IDs, names, engines, and dark mode support.\",\r\n annotations: {\r\n title: \"List Email Clients\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async () => {\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n EMAIL_CLIENTS.map((c) => ({\r\n id: c.id,\r\n name: c.name,\r\n category: c.category,\r\n engine: c.engine,\r\n darkModeSupport: c.darkModeSupport,\r\n ...(c.deprecated ? { deprecated: c.deprecated } : {}),\r\n })),\r\n null,\r\n 2,\r\n ),\r\n },\r\n ],\r\n };\r\n },\r\n);\r\n\r\n// ── Local Tool: diff_emails ────────────────────────────────────────\r\n\r\nserver.registerTool(\r\n \"diff_emails\",\r\n {\r\n title: \"Diff Emails\",\r\n description:\r\n \"Compare two email HTML versions, shows score changes, fixed issues, and newly introduced issues per client. Use after making fixes to verify improvements.\",\r\n inputSchema: {\r\n before: z.string().describe(\"Original email source\"),\r\n after: z.string().describe(\"Modified email source, in the same format\"),\r\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets.\"),\r\n },\r\n annotations: {\r\n title: \"Diff Emails\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async ({ before, after, format }) => {\r\n const beforeSizeError = validateHtmlSize(before);\r\n if (beforeSizeError) return beforeSizeError;\r\n const afterSizeError = validateHtmlSize(after);\r\n if (afterSizeError) return afterSizeError;\r\n\r\n const beforeSource = await toHtml(before, format);\r\n if (!beforeSource.ok) return mcpError(beforeSource.message);\r\n const afterSource = await toHtml(after, format);\r\n if (!afterSource.ok) return mcpError(afterSource.message);\r\n\r\n const framework = toFramework(format);\r\n\r\n const beforeWarnings = analyzeEmail(beforeSource.html, framework);\r\n const beforeScores = generateCompatibilityScore(beforeWarnings);\r\n\r\n const afterWarnings = analyzeEmail(afterSource.html, framework);\r\n const afterScores = generateCompatibilityScore(afterWarnings);\r\n\r\n const results = diffResults(\r\n { scores: beforeScores, warnings: beforeWarnings },\r\n { scores: afterScores, warnings: afterWarnings },\r\n );\r\n\r\n let clientsImproved = 0;\r\n let clientsRegressed = 0;\r\n let clientsUnchanged = 0;\r\n let totalDelta = 0;\r\n\r\n const mappedResults = results.map((r) => {\r\n if (r.scoreDelta > 0) clientsImproved++;\r\n else if (r.scoreDelta < 0) clientsRegressed++;\r\n else clientsUnchanged++;\r\n totalDelta += r.scoreDelta;\r\n\r\n return {\r\n clientId: r.clientId,\r\n scoreBefore: r.scoreBefore,\r\n scoreAfter: r.scoreAfter,\r\n scoreDelta: r.scoreDelta,\r\n fixed: r.fixed.map((w) => ({ property: w.property, client: w.client, message: w.message })),\r\n introduced: r.introduced.map((w) => ({ property: w.property, client: w.client, message: w.message })),\r\n unchanged: r.unchanged.length,\r\n };\r\n });\r\n\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n results: mappedResults,\r\n summary: {\r\n clientsImproved,\r\n clientsRegressed,\r\n clientsUnchanged,\r\n avgScoreDelta: results.length > 0 ? Math.round((totalDelta / results.length) * 10) / 10 : 0,\r\n },\r\n },\r\n null,\r\n 2,\r\n ),\r\n },\r\n ],\r\n };\r\n },\r\n);\r\n\r\n// ── Local Tool: check_deliverability ───────────────────────────────\r\n\r\nserver.registerTool(\r\n \"check_deliverability\",\r\n {\r\n title: \"Check Deliverability\",\r\n description:\r\n \"Check email deliverability for a domain: SPF, DKIM, DMARC, MX, and BIMI records. Returns a score (0-100) and actionable issues. Uses DNS lookups (no API key needed).\",\r\n inputSchema: {\r\n domain: z\r\n .string()\r\n .min(3)\r\n .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"Invalid domain format\")\r\n .describe(\"Domain to check (e.g. 'company.com')\"),\r\n },\r\n annotations: {\r\n title: \"Check Deliverability\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: true,\r\n },\r\n },\r\n async ({ domain }) => {\r\n try {\r\n if (!_checkDeliverability) {\r\n const mod = await import(\"@emailens/engine/server\");\r\n _checkDeliverability = mod.checkDeliverability;\r\n }\r\n const report = await _checkDeliverability!(domain);\r\n return {\r\n content: [{ type: \"text\" as const, text: JSON.stringify(report, null, 2) }],\r\n };\r\n } catch (err) {\r\n const msg = (err as Error).message ?? String(err);\r\n if (msg.includes(\"Cannot find\") || msg.includes(\"cannot find\") || msg.includes(\"MODULE_NOT_FOUND\")) {\r\n return mcpError(\r\n \"check_deliverability requires @emailens/engine >=0.9.1 with the /server subpath. Update with: npm install @emailens/engine@latest\",\r\n );\r\n }\r\n return mcpError(`Deliverability check failed for \"${domain}\": ${msg}`);\r\n }\r\n },\r\n);\r\n\r\n// ── Hosted Tool: capture_screenshots ───────────────────────────────\r\n\r\nserver.registerTool(\r\n \"capture_screenshots\",\r\n {\r\n title: \"Capture Screenshots\",\r\n description:\r\n \"Capture real email screenshots across 21 clients (Gmail, Outlook, Apple Mail, etc.) with light and dark mode variants. Screenshots are rendered in real browsers and hosted on CDN. Requires EMAILENS_API_KEY env var \\u2014 free plan at emailens.dev?ref=mcp.\",\r\n inputSchema: {\r\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\r\n format: formatEnum.describe(\"Input format\"),\r\n clients: z.array(z.string()).optional().describe(\"Filter to specific client IDs\"),\r\n modes: z\r\n .array(z.enum([\"light\", \"dark\"]))\r\n .optional()\r\n .describe(\"Screenshot modes (default: ['light'])\"),\r\n title: z.string().optional().describe(\"Name for the hosted preview\"),\r\n },\r\n annotations: {\r\n title: \"Capture Screenshots\",\r\n readOnlyHint: false,\r\n destructiveHint: false,\r\n idempotentHint: false,\r\n openWorldHint: true,\r\n },\r\n },\r\n async ({ html, format, clients, modes, title }) => {\r\n if (!config.isHosted) {\r\n return noApiKeyError(\"capture_screenshots\");\r\n }\r\n\r\n const opts = { apiKey: config.apiKey!, apiUrl: config.apiUrl };\r\n\r\n // Step 1: Create hosted preview\r\n const previewResult = await apiRequest<{\r\n id: string;\r\n overallScore: number;\r\n }>(\"/api/preview\", {\r\n ...opts,\r\n body: { html, format, clients, title },\r\n timeoutMs: 30_000,\r\n });\r\n\r\n if (previewResult.isError) {\r\n return mcpError(previewResult.error!);\r\n }\r\n\r\n const previewId = previewResult.data!.id;\r\n\r\n // Step 2: Capture screenshots (with one retry on timeout)\r\n let screenshotResult = await apiRequest<{\r\n screenshots: Record<string, string>;\r\n screenshotStatus: string;\r\n captured: string[];\r\n failed: string[];\r\n }>(`/api/preview/${previewId}/screenshots`, {\r\n ...opts,\r\n body: { clients, modes },\r\n timeoutMs: 120_000,\r\n });\r\n\r\n // Retry once on timeout\r\n if (screenshotResult.isError && screenshotResult.error?.includes(\"timed out\")) {\r\n screenshotResult = await apiRequest(`/api/preview/${previewId}/screenshots`, {\r\n ...opts,\r\n body: { clients, modes },\r\n timeoutMs: 120_000,\r\n });\r\n }\r\n\r\n // If screenshots still failed, return preview analysis with error note\r\n if (screenshotResult.isError) {\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n previewId,\r\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\r\n overallScore: previewResult.data!.overallScore,\r\n screenshots: {},\r\n screenshotStatus: \"failed\",\r\n captured: [],\r\n failed: [],\r\n note: `Preview created but screenshot capture failed: ${screenshotResult.error}. View the full preview at the URL above.`,\r\n },\r\n null,\r\n 2,\r\n ),\r\n },\r\n ],\r\n };\r\n }\r\n\r\n const ss = screenshotResult.data!;\r\n const result: Record<string, unknown> = {\r\n previewId,\r\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\r\n overallScore: previewResult.data!.overallScore,\r\n screenshots: ss.screenshots,\r\n screenshotStatus: ss.screenshotStatus,\r\n captured: ss.captured,\r\n failed: ss.failed,\r\n };\r\n\r\n if (ss.failed?.length > 0) {\r\n result.note = `${ss.failed.length} of ${(ss.captured?.length ?? 0) + ss.failed.length} screenshots failed: ${ss.failed.join(\", \")}`;\r\n }\r\n\r\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] };\r\n },\r\n);\r\n\r\n// ── Hosted Tool: share_preview ─────────────────────────────────────\r\n\r\nserver.registerTool(\r\n \"share_preview\",\r\n {\r\n title: \"Share Preview\",\r\n description:\r\n \"Create a shareable link for an email preview. Recipients see the full analysis without needing an account. Requires EMAILENS_API_KEY env var and Dev plan ($9/mo).\",\r\n inputSchema: {\r\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\r\n title: z.string().optional().describe(\"Display title for the share page\"),\r\n format: formatEnum.describe(\"Input format\"),\r\n },\r\n annotations: {\r\n title: \"Share Preview\",\r\n readOnlyHint: false,\r\n destructiveHint: false,\r\n idempotentHint: false,\r\n openWorldHint: true,\r\n },\r\n },\r\n async ({ html, title, format }) => {\r\n if (!config.isHosted) {\r\n return noApiKeyError(\"share_preview\", \"Shareable links require the Dev plan ($9/mo).\");\r\n }\r\n\r\n const result = await apiRequest<{ id: string; expiresAt: string | null }>(\"/api/share\", {\r\n apiKey: config.apiKey!,\r\n apiUrl: config.apiUrl,\r\n body: { html, title, format },\r\n });\r\n\r\n if (result.isError) {\r\n return mcpError(result.error!);\r\n }\r\n\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n shareUrl: `${config.apiUrl}/share/${result.data!.id}`,\r\n expiresAt: result.data!.expiresAt,\r\n },\r\n null,\r\n 2,\r\n ),\r\n },\r\n ],\r\n };\r\n },\r\n);\r\n\r\n// ── Start ────────────────────────────────────────────────────────────\r\nconst transport = new StdioServerTransport();\r\nawait server.connect(transport);\r\n","import { EMAIL_CLIENTS, type CSSWarning, type SourceLocation } from \"@emailens/engine\";\r\n\r\n/**\r\n * Turning the engine's per-client warnings into something a model can afford\r\n * to read.\r\n *\r\n * The engine reports per client because a score is per client, and per selector\r\n * because a fix is per selector. On an ordinary 11KB newsletter that is 347\r\n * warnings, which collapse to **46 distinct property-and-severity pairs**.\r\n * `border-radius` alone arrives twelve times: two clients that drop it, six\r\n * selectors that use it, the same sentence and the same VML workaround in every\r\n * one. Serialised with the fix snippets that is 286KB, about 73,000 tokens, for\r\n * an email the size of a README.\r\n *\r\n * A tool that cannot be called twice in a conversation gets called once and\r\n * then avoided, so this collapses the duplication:\r\n *\r\n * - **One entry per property and severity**, listing the clients it affects.\r\n * The clients are the data; repeating the prose once per client is not.\r\n * - **The message loses the client's name**, which is the only thing that\r\n * differed between the copies. It is stripped by exact match against the\r\n * client's own display name rather than by pattern, so it cannot eat a real\r\n * word.\r\n * - **No `fix` snippets.** They were 93KB of the 286KB, and `fix_email` exists\r\n * precisely to produce them for the issues a caller decides to act on.\r\n * - **Positions merge.** Every place the property breaks, across every client\r\n * and selector, in document order.\r\n *\r\n * `detail: \"full\"` returns the engine's own per-client shape, unchanged, for a\r\n * caller that genuinely wants it.\r\n */\r\n\r\nconst CLIENT_NAMES = new Map(EMAIL_CLIENTS.map((c) => [c.id, c.name]));\r\n\r\n/** One problem, and everywhere it lands. */\r\nexport interface CollapsedFinding {\r\n property: string;\r\n severity: CSSWarning[\"severity\"];\r\n /** Client ids affected, in the matrix's own order. */\r\n clients: string[];\r\n /** The engine's message with the client's name removed. */\r\n message: string;\r\n suggestion?: string;\r\n /** `structural` means the fix changes markup, not just CSS. */\r\n fixType?: CSSWarning[\"fixType\"];\r\n /** True when `fix_email` can produce a snippet for this. */\r\n hasFix?: boolean;\r\n loc?: Pick<SourceLocation, \"line\" | \"column\" | \"offset\" | \"length\">;\r\n alsoAtLines?: number[];\r\n locsTruncated?: boolean;\r\n}\r\n\r\n/**\r\n * Strip the leading client name from a message.\r\n *\r\n * Messages read \"Outlook (New) does not support …\" / \"Yahoo Mail has partial\r\n * support for …\". Matching the client's own display name exactly is safer than\r\n * a pattern: \"Mail\" and \"Outlook\" appear inside real sentences.\r\n */\r\nfunction withoutClientName(message: string, clientId: string): string {\r\n const name = CLIENT_NAMES.get(clientId);\r\n if (!name || !message.startsWith(name)) return message;\r\n const rest = message.slice(name.length).trimStart();\r\n return rest ? rest.charAt(0).toUpperCase() + rest.slice(1) : message;\r\n}\r\n\r\n/** Positions from every warning in a group, deduplicated, in document order. */\r\nfunction mergeLocations(group: CSSWarning[]): SourceLocation[] {\r\n const byOffset = new Map<number, SourceLocation>();\r\n for (const w of group) {\r\n for (const loc of w.locs ?? (w.loc ? [w.loc] : [])) {\r\n if (!byOffset.has(loc.offset)) byOffset.set(loc.offset, loc);\r\n }\r\n }\r\n return [...byOffset.values()].sort((a, b) => a.offset - b.offset);\r\n}\r\n\r\n/**\r\n * Collapse per-client, per-selector warnings into one entry each.\r\n *\r\n * Grouped by property, severity **and message**: two clients can both drop a\r\n * property for different reasons, and merging those would attribute a caveat\r\n * to a client whose note never said it.\r\n */\r\nexport function collapseWarnings(warnings: CSSWarning[]): CollapsedFinding[] {\r\n const groups = new Map<string, CSSWarning[]>();\r\n for (const w of warnings) {\r\n // A NUL separator, because it cannot occur in a property, a severity or\r\n // a message; a space could, and two different triples would collide.\r\n const key = [w.property, w.severity, withoutClientName(w.message, w.client)].join(\"\\u0000\");\r\n const seen = groups.get(key);\r\n if (seen) seen.push(w);\r\n else groups.set(key, [w]);\r\n }\r\n\r\n return [...groups.values()].map((group) => {\r\n const first = group[0];\r\n const clients = [...new Set(group.map((w) => w.client))];\r\n const locs = mergeLocations(group);\r\n // A suggestion can differ per client (5 of 46 groups on the reference\r\n // fixture). Keeping the first would silently attribute one client's advice\r\n // to another, so a group that disagrees says nothing and lets the caller\r\n // ask `fix_email`.\r\n const suggestions = new Set(group.map((w) => w.suggestion ?? \"\"));\r\n const suggestion = suggestions.size === 1 ? first.suggestion : undefined;\r\n\r\n return {\r\n property: first.property,\r\n severity: first.severity,\r\n clients,\r\n message: withoutClientName(first.message, first.client),\r\n ...(suggestion ? { suggestion } : {}),\r\n ...(first.fixType ? { fixType: first.fixType } : {}),\r\n ...(group.some((w) => w.fix) ? { hasFix: true } : {}),\r\n ...(locs.length\r\n ? {\r\n loc: {\r\n line: locs[0].line,\r\n column: locs[0].column,\r\n offset: locs[0].offset,\r\n length: locs[0].length,\r\n },\r\n // Lines, not positions, and each line once: two occurrences on\r\n // one line are two offsets but one place to look.\r\n ...(() => {\r\n const lines = [...new Set(locs.slice(1).map((l) => l.line))].filter(\r\n (line) => line !== locs[0].line,\r\n );\r\n return lines.length ? { alsoAtLines: lines } : {};\r\n })(),\r\n }\r\n : {}),\r\n ...(group.some((w) => w.locsTruncated) ? { locsTruncated: true } : {}),\r\n };\r\n });\r\n}\r\n\r\n/** Keep only the warnings for these clients. An empty or absent list keeps all. */\r\nexport function forClients(warnings: CSSWarning[], clients?: string[]): CSSWarning[] {\r\n if (!clients?.length) return warnings;\r\n const wanted = new Set(clients);\r\n return warnings.filter((w) => wanted.has(w.client));\r\n}\r\n\r\n/** Every client id the engine knows, for validating a caller's filter. */\r\nexport function knownClientIds(): string[] {\r\n return EMAIL_CLIENTS.map((c) => c.id);\r\n}\r\n","import { compile } from \"@emailens/engine/compile\";\n\n/**\n * Getting a template to HTML before anything looks at it.\n *\n * The `format` parameter used to do nothing but choose the syntax of the fix\n * snippets, while the source was handed to the analyzer as-is. That is a quiet\n * and expensive lie: cheerio parses `<mjml><mj-section>…` without complaint,\n * finds no CSS in it, and the tools answer **zero findings**. An assistant\n * reads that as a clean bill of health and says the campaign is fine. A wrong\n * answer delivered confidently is worse than an error.\n *\n * So a non-HTML format is compiled first, with the same compilers the CLI uses.\n * They are optional peer dependencies of the engine (MJML alone is 56MB, and\n * this server is usually run through `npx`), so a caller can land on a machine\n * that has none of them. That case gets the engine's own \"install this\" message\n * plus the way out that needs nothing installed: compile it yourself and send\n * the HTML.\n */\n\nexport type SourceResult = { ok: true; html: string } | { ok: false; message: string };\n\nconst COMPILED: ReadonlySet<string> = new Set([\"jsx\", \"mjml\", \"maizzle\"]);\n\n/**\n * Is this \"the package isn't here\" rather than \"the template is wrong\"?\n *\n * The engine says `MJML compilation requires \"mjml\"` for a missing compiler\n * and `Sandbox strategy \"isolated-vm\" requires the \"isolated-vm\" package` when\n * the compiler is present but its sandbox is not, which is what React Email\n * hits first. Both are the same problem for the caller, so one pattern covers\n * them. Getting this wrong is cheap in one direction only: a misclassified\n * template error gains an irrelevant sentence, while a misclassified missing\n * package would tell someone to debug markup that is fine.\n */\nfunction isMissingCompiler(message: string): boolean {\n return /requires (?:the )?\"/.test(message);\n}\n\n/**\n * Compile `source` when `format` names a template language, or pass it through\n * when it is already HTML.\n */\nexport async function toHtml(source: string, format?: string): Promise<SourceResult> {\n if (!format || format === \"html\") return { ok: true, html: source };\n if (!COMPILED.has(format)) {\n return { ok: false, message: `Unknown format \"${format}\". Use html, jsx, mjml, or maizzle.` };\n }\n\n try {\n return { ok: true, html: await compile(source, format as \"jsx\" | \"mjml\" | \"maizzle\") };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (isMissingCompiler(message)) {\n return {\n ok: false,\n message:\n `${message}\\n\\nThat has to happen where this MCP server runs, which may not be somewhere ` +\n `you can install to. If it is not, compile the template yourself and send the resulting ` +\n `HTML with format \"html\".`,\n };\n }\n return { ok: false, message };\n }\n}\n","export const config = {\n get apiKey(): string | null {\n return process.env.EMAILENS_API_KEY ?? null;\n },\n get apiUrl(): string {\n return process.env.EMAILENS_API_URL ?? \"https://emailens.dev\";\n },\n get isHosted(): boolean {\n return this.apiKey !== null;\n },\n};\n","export interface ApiRequestOptions {\n apiKey: string;\n apiUrl: string;\n method?: \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\";\n body?: unknown;\n timeoutMs?: number;\n}\n\nexport interface ApiResult<T = unknown> {\n data?: T;\n isError?: true;\n error?: string;\n}\n\nexport async function apiRequest<T = unknown>(\n path: string,\n options: ApiRequestOptions,\n): Promise<ApiResult<T>> {\n const { apiKey, apiUrl, method = \"GET\", body, timeoutMs = 30_000 } = options;\n const url = `${apiUrl}${path}`;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: method !== \"GET\" ? method : body ? \"POST\" : \"GET\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n if (response.status === 401) {\n return {\n isError: true,\n error: \"Invalid or revoked API key. Generate a new one at emailens.dev/settings/api-keys?ref=mcp\",\n };\n }\n\n if (response.status === 403) {\n const data = (await response.json().catch(() => ({}))) as Record<string, unknown>;\n return {\n isError: true,\n error: data.upgrade\n ? \"This feature requires a plan upgrade. Visit emailens.dev/pricing?ref=mcp\"\n : \"Access denied. Check your plan at emailens.dev/pricing?ref=mcp\",\n };\n }\n\n if (response.status === 429) {\n const data = (await response.json().catch(() => ({}))) as Record<string, number>;\n return {\n isError: true,\n error: `Daily quota exceeded${data.limit ? ` (${data.limit - (data.remaining ?? 0)}/${data.limit})` : \"\"}. Resets tomorrow. Upgrade at emailens.dev/pricing?ref=mcp`,\n };\n }\n\n if (!response.ok) {\n const data = (await response.json().catch(() => ({}))) as Record<string, unknown>;\n return { isError: true, error: (data.error as string) ?? `API error: ${response.status}` };\n }\n\n const data = (await response.json()) as T;\n return { data };\n } catch (err) {\n if ((err as Error).name === \"AbortError\") {\n return { isError: true, error: `Request timed out after ${timeoutMs / 1000}s. Try again or check emailens.dev status.` };\n }\n return { isError: true, error: `Network error: ${(err as Error).message}` };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function mcpError(message: string) {\n return {\n content: [{ type: \"text\" as const, text: message }],\n isError: true,\n };\n}\n\nexport function noApiKeyError(toolName: string, extraInfo?: string) {\n const lines = [\n `${toolName} requires an Emailens API key.`,\n \"\",\n \"1. Sign up free at emailens.dev?ref=mcp\",\n \"2. Create a key at emailens.dev/settings/api-keys?ref=mcp\",\n \"3. Add to your MCP config:\",\n \"\",\n ' \"env\": { \"EMAILENS_API_KEY\": \"ek_live_...\" }',\n ];\n if (extraInfo) {\n lines.push(\"\", extraInfo);\n } else {\n lines.push(\"\", \"Free plan includes 30 previews/day (each preview can include screenshots).\");\n }\n return mcpError(lines.join(\"\\n\"));\n}\n"],"mappings":"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","names":["EMAIL_CLIENTS","data","EMAIL_CLIENTS"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/findings.ts","../src/compile.ts","../src/config.ts","../src/hosted.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { collapseWarnings, forClients, knownClientIds } from \"./findings.js\";\nimport { resolveSourceFormat, toHtml } from \"./compile.js\";\nimport {\n createSession,\n analyzeEmail,\n generateCompatibilityScore,\n simulateDarkMode,\n transformForAllClients,\n transformForClient,\n generateFixPrompt,\n estimateAiFixTokens,\n diffResults,\n toPlainText,\n EMAIL_CLIENTS,\n MAX_HTML_SIZE,\n type Framework,\n type CSSWarning,\n type SourceLocation,\n} from \"@emailens/engine\";\n// Lazy-imported: @emailens/engine/server may not exist in all engine versions\nlet _checkDeliverability: ((domain: string) => Promise<unknown>) | null = null;\nimport { config } from \"./config.js\";\nimport { apiRequest, mcpError, noApiKeyError } from \"./hosted.js\";\n\nfunction toFramework(format?: string): Framework | undefined {\n if (format === \"jsx\" || format === \"mjml\" || format === \"maizzle\") return format;\n return undefined;\n}\n\n/** The headline is the worst client. A mean of 21 clients hides the one that fails. */\nfunction worstScore(scores: Record<string, { score: number }>): number {\n const values = Object.values(scores);\n if (values.length === 0) return 0;\n return Math.min(...values.map((s) => s.score));\n}\n\n/**\n * The language the source is written in.\n *\n * Anything but `html` is compiled before analysis and also selects the syntax\n * the fix snippets are written in.\n */\nconst formatEnum = z.enum([\"html\", \"jsx\", \"mjml\", \"maizzle\"]).optional();\n\n/**\n * How much compatibility detail to return.\n *\n * `summary` collapses the engine's per-client, per-selector warnings into one\n * entry per problem listing the clients it affects: the same information at a\n * fraction of the tokens. `full` is the engine's own shape, per client, with\n * fix snippets, for a caller that wants everything.\n */\nconst detailEnum = z\n .enum([\"summary\", \"full\"])\n .optional()\n .describe(\n \"'summary' (default) returns one entry per problem, listing the clients it affects (roughly 10x smaller). 'full' returns one entry per client with fix snippets.\",\n );\n\nconst targetingPolicyEnum = z\n .enum([\"progressive\", \"strict\", \"lenient\"])\n .optional()\n .describe(\n \"'progressive' (default) suppresses compatibility false-positives inside client-targeted scopes and only flags deprecated or dangerous hacks. 'strict' treats targeting as non-standard and reports every hack. 'lenient' suppresses false positives and ignores non-fatal targeting warnings.\",\n );\n\nconst skipEnum = z.enum([\n \"spam\",\n \"links\",\n \"accessibility\",\n \"images\",\n \"compatibility\",\n \"inboxPreview\",\n \"size\",\n \"templateVariables\",\n \"overflow\",\n \"visual\",\n \"darkContrast\",\n \"mobileContrast\",\n \"design\",\n \"vml\",\n \"styleSurvival\",\n \"targeting\",\n]);\n\nconst clientsParam = z\n .array(z.string())\n .optional()\n .describe(\n \"Only report these client IDs (e.g. ['gmail-web','outlook-windows']). The fastest way to cut the response when you care about specific clients. Use list_clients for the IDs.\",\n );\n\n/**\n * Reject a client id the engine does not know.\n *\n * Filtering to a typo would return an empty list, which reads as \"this email\n * is fine for that client\"; the most misleading answer the tool can give.\n */\nfunction validateClients(clients: string[] | undefined) {\n if (!clients?.length) return undefined;\n const known = new Set(knownClientIds());\n const unknown = clients.filter((c) => !known.has(c));\n if (!unknown.length) return undefined;\n return mcpError(\n `Unknown client ${unknown.length > 1 ? \"IDs\" : \"ID\"}: ${unknown.join(\", \")}. Call list_clients for the valid IDs.`,\n );\n}\n\n/** The compatibility payload, at the requested level of detail. */\nfunction compatibilityPayload(\n warnings: CSSWarning[],\n detail: \"summary\" | \"full\" | undefined,\n clients: string[] | undefined,\n) {\n const scoped = forClients(warnings, clients);\n if (detail === \"full\") {\n return {\n warningCount: scoped.length,\n warnings: scoped.map((w) => ({\n client: w.client,\n property: w.property,\n severity: w.severity,\n message: w.message,\n suggestion: w.suggestion,\n fix: w.fix,\n fixType: w.fixType,\n ...withPositions(w),\n })),\n };\n }\n const findings = collapseWarnings(scoped);\n return {\n warningCount: scoped.length,\n findingCount: findings.length,\n findings,\n };\n}\n\n/**\n * Do source positions refer to the code the caller handed us?\n *\n * Only for plain HTML. JSX, MJML and Maizzle are compiled before analysis, so a\n * position would point into generated output: worse than none, because an\n * agent would edit the wrong line with confidence.\n */\nfunction positionsApply(format?: string): boolean {\n return (format ?? \"html\") === \"html\";\n}\n\n/**\n * The position fields a finding carries, when it has them.\n *\n * Deliberately compact: this response is read by a model paying for every\n * token, and a newsletter can produce a thousand occurrences. The first one\n * gets a full position, enough to edit it, and the rest are reduced to line\n * numbers, which is all an assistant needs to go find them. Carrying every\n * occurrence in full nearly doubled the payload on a real fixture.\n */\nfunction withPositions<T extends {\n loc?: SourceLocation;\n locs?: SourceLocation[];\n locsTruncated?: boolean;\n}>(w: T) {\n if (!w.loc) return {};\n const { line, column, offset, length } = w.loc;\n const others = (w.locs ?? []).slice(1).map((l) => l.line);\n return {\n loc: { line, column, offset, length },\n ...(others.length ? { alsoAtLines: others } : {}),\n ...(w.locsTruncated ? { locsTruncated: true } : {}),\n };\n}\n\nfunction validateHtmlSize(html: string) {\n if (html.length > MAX_HTML_SIZE) {\n return mcpError(`HTML input exceeds ${Math.round(MAX_HTML_SIZE / 1024)}KB limit. Reduce the email size and try again.`);\n }\n return null;\n}\n\nconst server = new McpServer({\n name: \"emailens\",\n version: \"0.9.1\",\n});\n\n// ── Local Tool: preview_email ──────────────────────────────────────\n\nserver.registerTool(\n \"preview_email\",\n {\n title: \"Preview Email\",\n description:\n `Full email compatibility preview, transforms HTML for ${EMAIL_CLIENTS.length} email clients (Gmail, Outlook, Apple Mail, Yahoo, Samsung, Thunderbird, HEY, Proton Mail, AOL, Fastmail, Superhuman), analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.`,\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n clients: z\n .array(z.string())\n .optional()\n .describe(\"Optional client ID filter (e.g. ['gmail-web', 'outlook-windows'])\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n targetingPolicy: targetingPolicyEnum,\n },\n annotations: {\n title: \"Preview Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html, clients, format, targetingPolicy }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const validClientIds = new Set(EMAIL_CLIENTS.map((c) => c.id));\n const framework = toFramework(resolveSourceFormat(html, format));\n const session = createSession(source.html, { framework, targetingPolicy });\n\n let transforms;\n if (clients) {\n const filter = clients.filter((c) => validClientIds.has(c));\n if (filter.length === 0) {\n return mcpError(\n JSON.stringify({ error: \"No valid client IDs provided\", validClientIds: Array.from(validClientIds) }, null, 2),\n );\n }\n transforms = filter.map((c) => transformForClient(source.html, c, framework));\n } else {\n transforms = transformForAllClients(source.html, framework);\n }\n\n const warnings = session.analyze();\n const scores = generateCompatibilityScore(warnings);\n const inboxPreview = session.extractInboxPreview();\n const sizeReport = session.checkSize();\n\n const darkMode: Record<string, { html: string; warnings: CSSWarning[] }> = {};\n for (const t of transforms) {\n darkMode[t.clientId] = simulateDarkMode(source.html, t.clientId);\n }\n\n const overallScore = worstScore(scores);\n\n const plainText = toPlainText(source.html);\n\n const result: Record<string, unknown> = {\n overallScore,\n compatibilityScores: scores,\n cssWarnings: warnings.map((w) => ({\n client: w.client,\n property: w.property,\n severity: w.severity,\n message: w.message,\n suggestion: w.suggestion,\n fix: w.fix,\n fixType: w.fixType,\n })),\n inboxPreview,\n sizeReport,\n ...(plainText ? { plainText } : {}),\n clientCount: transforms.length,\n darkModeWarnings: Object.entries(darkMode).reduce(\n (acc, [clientId, dm]) => {\n if (dm.warnings.length > 0) acc[clientId] = dm.warnings.length;\n return acc;\n },\n {} as Record<string, number>,\n ),\n };\n\n // Contextual tip: only when no API key and score indicates issues\n if (!config.isHosted && overallScore < 90) {\n result.tip =\n \"Capture real screenshots to verify these issues across clients. Add EMAILENS_API_KEY to your MCP config \\u2192 emailens.dev/settings/api-keys?ref=mcp\";\n }\n\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] };\n },\n);\n\n// ── Local Tool: analyze_email (CSS-only, reverted from audit) ──────\n\nserver.registerTool(\n \"analyze_email\",\n {\n title: \"Analyze Email\",\n description:\n \"Quick CSS compatibility analysis, returns per-client scores and one finding per problem, listing the clients it affects. For HTML input each finding carries loc (line, column, offset) and alsoAtLines, so you can edit the exact source. Narrow with clients, or pass detail:'full' for the per-client breakdown with fix snippets. Use audit_email for a full quality report (spam, links, a11y, images, etc.).\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n detail: detailEnum,\n clients: clientsParam,\n targetingPolicy: targetingPolicyEnum,\n },\n annotations: {\n title: \"Analyze Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html, format, detail, clients, targetingPolicy }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n const clientError = validateClients(clients);\n if (clientError) return clientError;\n\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const warnings = analyzeEmail(source.html, toFramework(resolveSourceFormat(html, format)), {\n positions: positionsApply(resolveSourceFormat(html, format)),\n targetingPolicy,\n });\n // Scores stay whole-email: filtering to two clients should narrow what is\n // reported, not silently change what the email scores.\n const scores = generateCompatibilityScore(warnings);\n\n const overallScore = worstScore(scores);\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n overallScore,\n scores,\n ...compatibilityPayload(warnings, detail, clients),\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: audit_email ────────────────────────────────────────\n\nserver.registerTool(\n \"audit_email\",\n {\n title: \"Audit Email\",\n description:\n \"Comprehensive email quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual fallbacks, dark-mode and mobile text contrast, design consistency, structural faults in Outlook-only VML (which lives inside conditional comments and is invisible to every other check), style survival: CSS a client parses correctly and then discards, such as a stylesheet past Gmail's 16 KB ceiling or every rule after a `}}`, which no support matrix can express, and client targeting: HowToTarget hacks such as Gmail u+.body or MSO conditionals, which do not score as css-hack. For HTML input, findings tied to a specific element carry loc (line, column, offset) so you can edit the exact source. Compatibility is collapsed to one finding per problem listing the clients it affects; pass detail:'full' for the per-client breakdown with fix snippets. Use skip to omit checks, targetingPolicy to change how targeted scopes affect scores, and clients to narrow which clients are reported.\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n skip: z\n .array(skipEnum)\n .optional()\n .describe(\"Checks to skip (e.g. ['spam', 'images'])\"),\n detail: detailEnum,\n clients: clientsParam,\n targetingPolicy: targetingPolicyEnum,\n },\n annotations: {\n title: \"Audit Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html, format, skip, detail, clients, targetingPolicy }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n const clientError = validateClients(clients);\n if (clientError) return clientError;\n\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const session = createSession(source.html, {\n framework: toFramework(resolveSourceFormat(html, format)),\n positions: positionsApply(resolveSourceFormat(html, format)),\n targetingPolicy,\n });\n const report = session.audit({ skip });\n\n const overallCompatibility = worstScore(report.compatibility.scores);\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n overallCompatibility,\n compatibility: {\n scores: report.compatibility.scores,\n ...compatibilityPayload(report.compatibility.warnings, detail, clients),\n },\n spam: report.spam,\n links: report.links,\n accessibility: report.accessibility,\n images: report.images,\n inboxPreview: report.inboxPreview,\n size: report.size,\n templateVariables: report.templateVariables,\n overflow: report.overflow,\n visual: report.visual,\n vml: report.vml,\n darkContrast: report.darkContrast,\n mobileContrast: report.mobileContrast,\n design: report.design,\n styleSurvival: report.styleSurvival,\n targeting: report.targeting,\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: fix_email ──────────────────────────────────────────\n\nserver.registerTool(\n \"fix_email\",\n {\n title: \"Fix Email\",\n description:\n \"Generate a structured fix prompt for email compatibility issues. Returns markdown with the original code, detected issues (CSS or structural), fix snippets, and format-specific instructions. Use after preview_email or analyze_email.\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so: the source to fix\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n scope: z.enum([\"all\", \"current\"]).optional().describe(\"'all' (default) or 'current' (requires selectedClientId)\"),\n selectedClientId: z.string().optional().describe(\"Client ID to scope fixes to (e.g. 'outlook-windows')\"),\n },\n annotations: {\n title: \"Fix Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html, format, scope, selectedClientId }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n\n // The findings come from the compiled output, because that is what a\n // client renders. The prompt keeps the caller's own source, because that is\n // the file they will edit; a fix written against generated HTML is not a\n // fix they can apply.\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const resolved = resolveSourceFormat(html, format) as\n | \"html\"\n | \"jsx\"\n | \"mjml\"\n | \"maizzle\";\n const framework = toFramework(resolved);\n const fixScope = scope === \"current\" ? \"current\" : \"all\";\n const inputFormat = resolved;\n\n const warnings = analyzeEmail(source.html, framework);\n const scores = generateCompatibilityScore(warnings);\n\n if (warnings.length === 0) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify({ message: \"No compatibility issues found \\u2014 nothing to fix.\", overallScore: 100 }, null, 2),\n },\n ],\n };\n }\n\n const estimate = await estimateAiFixTokens({\n originalHtml: html,\n warnings,\n scores,\n scope: fixScope,\n selectedClientId,\n format: inputFormat,\n });\n\n const prompt = generateFixPrompt({\n originalHtml: html,\n warnings,\n scores,\n scope: fixScope,\n selectedClientId,\n format: inputFormat,\n });\n\n const structural = warnings.filter((w) => w.fixType === \"structural\");\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n totalWarnings: warnings.length,\n structuralWarnings: structural.length,\n cssWarnings: warnings.length - structural.length,\n tokenEstimate: {\n inputTokens: estimate.inputTokens,\n estimatedOutputTokens: estimate.estimatedOutputTokens,\n truncated: estimate.truncated,\n },\n note: \"Apply the fixes in the prompt below. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes.\",\n },\n null,\n 2,\n ),\n },\n { type: \"text\" as const, text: prompt },\n ],\n };\n },\n);\n\n// ── Local Tool: list_clients ───────────────────────────────────────\n\nserver.registerTool(\n \"list_clients\",\n {\n title: \"List Email Clients\",\n description: \"List every supported email client with IDs, names, engines, and dark mode support.\",\n annotations: {\n title: \"List Email Clients\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async () => {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n EMAIL_CLIENTS.map((c) => ({\n id: c.id,\n name: c.name,\n category: c.category,\n engine: c.engine,\n darkModeSupport: c.darkModeSupport,\n ...(c.deprecated ? { deprecated: c.deprecated } : {}),\n })),\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: diff_emails ────────────────────────────────────────\n\nserver.registerTool(\n \"diff_emails\",\n {\n title: \"Diff Emails\",\n description:\n \"Compare two email HTML versions, shows score changes, fixed issues, and newly introduced issues per client. Use after making fixes to verify improvements.\",\n inputSchema: {\n before: z.string().describe(\"Original email source\"),\n after: z.string().describe(\"Modified email source, in the same format\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n },\n annotations: {\n title: \"Diff Emails\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ before, after, format }) => {\n const beforeSizeError = validateHtmlSize(before);\n if (beforeSizeError) return beforeSizeError;\n const afterSizeError = validateHtmlSize(after);\n if (afterSizeError) return afterSizeError;\n\n const beforeSource = await toHtml(before, format);\n if (!beforeSource.ok) return mcpError(beforeSource.message);\n const afterSource = await toHtml(after, format);\n if (!afterSource.ok) return mcpError(afterSource.message);\n\n const framework = toFramework(\n resolveSourceFormat(before, format) === \"maizzle\" ||\n resolveSourceFormat(after, format) === \"maizzle\"\n ? (format ?? \"maizzle\")\n : (format ?? \"html\"),\n );\n\n const beforeWarnings = analyzeEmail(beforeSource.html, framework);\n const beforeScores = generateCompatibilityScore(beforeWarnings);\n\n const afterWarnings = analyzeEmail(afterSource.html, framework);\n const afterScores = generateCompatibilityScore(afterWarnings);\n\n const results = diffResults(\n { scores: beforeScores, warnings: beforeWarnings },\n { scores: afterScores, warnings: afterWarnings },\n );\n\n let clientsImproved = 0;\n let clientsRegressed = 0;\n let clientsUnchanged = 0;\n let totalDelta = 0;\n\n const mappedResults = results.map((r) => {\n if (r.scoreDelta > 0) clientsImproved++;\n else if (r.scoreDelta < 0) clientsRegressed++;\n else clientsUnchanged++;\n totalDelta += r.scoreDelta;\n\n return {\n clientId: r.clientId,\n scoreBefore: r.scoreBefore,\n scoreAfter: r.scoreAfter,\n scoreDelta: r.scoreDelta,\n fixed: r.fixed.map((w) => ({ property: w.property, client: w.client, message: w.message })),\n introduced: r.introduced.map((w) => ({ property: w.property, client: w.client, message: w.message })),\n unchanged: r.unchanged.length,\n };\n });\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n results: mappedResults,\n summary: {\n clientsImproved,\n clientsRegressed,\n clientsUnchanged,\n avgScoreDelta: results.length > 0 ? Math.round((totalDelta / results.length) * 10) / 10 : 0,\n },\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: check_deliverability ───────────────────────────────\n\nserver.registerTool(\n \"check_deliverability\",\n {\n title: \"Check Deliverability\",\n description:\n \"Check email deliverability for a domain: SPF, DKIM, DMARC, MX, and BIMI records. Returns a score (0-100) and actionable issues. Uses DNS lookups (no API key needed).\",\n inputSchema: {\n domain: z\n .string()\n .min(3)\n .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"Invalid domain format\")\n .describe(\"Domain to check (e.g. 'company.com')\"),\n },\n annotations: {\n title: \"Check Deliverability\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ domain }) => {\n try {\n if (!_checkDeliverability) {\n const mod = await import(\"@emailens/engine/server\");\n _checkDeliverability = mod.checkDeliverability;\n }\n const report = await _checkDeliverability!(domain);\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(report, null, 2) }],\n };\n } catch (err) {\n const msg = (err as Error).message ?? String(err);\n if (msg.includes(\"Cannot find\") || msg.includes(\"cannot find\") || msg.includes(\"MODULE_NOT_FOUND\")) {\n return mcpError(\n \"check_deliverability requires @emailens/engine >=0.9.1 with the /server subpath. Update with: npm install @emailens/engine@latest\",\n );\n }\n return mcpError(`Deliverability check failed for \"${domain}\": ${msg}`);\n }\n },\n);\n\n// ── Hosted Tool: capture_screenshots ───────────────────────────────\n\nserver.registerTool(\n \"capture_screenshots\",\n {\n title: \"Capture Screenshots\",\n description:\n \"Capture real email screenshots across 21 clients (Gmail, Outlook, Apple Mail, etc.) with light and dark mode variants. Screenshots are rendered in real browsers and hosted on CDN. Requires EMAILENS_API_KEY env var \\u2014 free plan at emailens.dev?ref=mcp.\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n format: formatEnum.describe(\"Input format\"),\n clients: z.array(z.string()).optional().describe(\"Filter to specific client IDs\"),\n modes: z\n .array(z.enum([\"light\", \"dark\"]))\n .optional()\n .describe(\"Screenshot modes (default: ['light'])\"),\n title: z.string().optional().describe(\"Name for the hosted preview\"),\n },\n annotations: {\n title: \"Capture Screenshots\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ html, format, clients, modes, title }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"capture_screenshots\");\n }\n\n const opts = { apiKey: config.apiKey!, apiUrl: config.apiUrl };\n\n // Step 1: Create hosted preview\n const previewResult = await apiRequest<{\n id: string;\n overallScore: number;\n }>(\"/api/preview\", {\n ...opts,\n body: { html, format, clients, title },\n timeoutMs: 30_000,\n });\n\n if (previewResult.isError) {\n return mcpError(previewResult.error!);\n }\n\n const previewId = previewResult.data!.id;\n\n // Step 2: Capture screenshots (with one retry on timeout)\n let screenshotResult = await apiRequest<{\n screenshots: Record<string, string>;\n screenshotStatus: string;\n captured: string[];\n failed: string[];\n }>(`/api/preview/${previewId}/screenshots`, {\n ...opts,\n body: { clients, modes },\n timeoutMs: 120_000,\n });\n\n // Retry once on timeout\n if (screenshotResult.isError && screenshotResult.error?.includes(\"timed out\")) {\n screenshotResult = await apiRequest(`/api/preview/${previewId}/screenshots`, {\n ...opts,\n body: { clients, modes },\n timeoutMs: 120_000,\n });\n }\n\n // If screenshots still failed, return preview analysis with error note\n if (screenshotResult.isError) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n previewId,\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\n overallScore: previewResult.data!.overallScore,\n screenshots: {},\n screenshotStatus: \"failed\",\n captured: [],\n failed: [],\n note: `Preview created but screenshot capture failed: ${screenshotResult.error}. View the full preview at the URL above.`,\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n const ss = screenshotResult.data!;\n const result: Record<string, unknown> = {\n previewId,\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\n overallScore: previewResult.data!.overallScore,\n screenshots: ss.screenshots,\n screenshotStatus: ss.screenshotStatus,\n captured: ss.captured,\n failed: ss.failed,\n };\n\n if (ss.failed?.length > 0) {\n result.note = `${ss.failed.length} of ${(ss.captured?.length ?? 0) + ss.failed.length} screenshots failed: ${ss.failed.join(\", \")}`;\n }\n\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] };\n },\n);\n\n// ── Hosted Tool: share_preview ─────────────────────────────────────\n\nserver.registerTool(\n \"share_preview\",\n {\n title: \"Share Preview\",\n description:\n \"Create a shareable link for an email preview. Recipients see the full analysis without needing an account. Requires EMAILENS_API_KEY env var and Dev plan ($9/mo).\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n title: z.string().optional().describe(\"Display title for the share page\"),\n format: formatEnum.describe(\"Input format\"),\n },\n annotations: {\n title: \"Share Preview\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ html, title, format }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"share_preview\", \"Shareable links require the Dev plan ($9/mo).\");\n }\n\n const result = await apiRequest<{ id: string; expiresAt: string | null }>(\"/api/share\", {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n body: { html, title, format },\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n shareUrl: `${config.apiUrl}/share/${result.data!.id}`,\n expiresAt: result.data!.expiresAt,\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Start ────────────────────────────────────────────────────────────\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n","import { EMAIL_CLIENTS, type CSSWarning, type SourceLocation } from \"@emailens/engine\";\r\n\r\n/**\r\n * Turning the engine's per-client warnings into something a model can afford\r\n * to read.\r\n *\r\n * The engine reports per client because a score is per client, and per selector\r\n * because a fix is per selector. On an ordinary 11KB newsletter that is 347\r\n * warnings, which collapse to **46 distinct property-and-severity pairs**.\r\n * `border-radius` alone arrives twelve times: two clients that drop it, six\r\n * selectors that use it, the same sentence and the same VML workaround in every\r\n * one. Serialised with the fix snippets that is 286KB, about 73,000 tokens, for\r\n * an email the size of a README.\r\n *\r\n * A tool that cannot be called twice in a conversation gets called once and\r\n * then avoided, so this collapses the duplication:\r\n *\r\n * - **One entry per property and severity**, listing the clients it affects.\r\n * The clients are the data; repeating the prose once per client is not.\r\n * - **The message loses the client's name**, which is the only thing that\r\n * differed between the copies. It is stripped by exact match against the\r\n * client's own display name rather than by pattern, so it cannot eat a real\r\n * word.\r\n * - **No `fix` snippets.** They were 93KB of the 286KB, and `fix_email` exists\r\n * precisely to produce them for the issues a caller decides to act on.\r\n * - **Positions merge.** Every place the property breaks, across every client\r\n * and selector, in document order.\r\n *\r\n * `detail: \"full\"` returns the engine's own per-client shape, unchanged, for a\r\n * caller that genuinely wants it.\r\n */\r\n\r\nconst CLIENT_NAMES = new Map(EMAIL_CLIENTS.map((c) => [c.id, c.name]));\r\n\r\n/** One problem, and everywhere it lands. */\r\nexport interface CollapsedFinding {\r\n property: string;\r\n severity: CSSWarning[\"severity\"];\r\n /** Client ids affected, in the matrix's own order. */\r\n clients: string[];\r\n /** The engine's message with the client's name removed. */\r\n message: string;\r\n suggestion?: string;\r\n /** `structural` means the fix changes markup, not just CSS. */\r\n fixType?: CSSWarning[\"fixType\"];\r\n /** True when `fix_email` can produce a snippet for this. */\r\n hasFix?: boolean;\r\n loc?: Pick<SourceLocation, \"line\" | \"column\" | \"offset\" | \"length\">;\r\n alsoAtLines?: number[];\r\n locsTruncated?: boolean;\r\n}\r\n\r\n/**\r\n * Strip the leading client name from a message.\r\n *\r\n * Messages read \"Outlook (New) does not support …\" / \"Yahoo Mail has partial\r\n * support for …\". Matching the client's own display name exactly is safer than\r\n * a pattern: \"Mail\" and \"Outlook\" appear inside real sentences.\r\n */\r\nfunction withoutClientName(message: string, clientId: string): string {\r\n const name = CLIENT_NAMES.get(clientId);\r\n if (!name || !message.startsWith(name)) return message;\r\n const rest = message.slice(name.length).trimStart();\r\n return rest ? rest.charAt(0).toUpperCase() + rest.slice(1) : message;\r\n}\r\n\r\n/** Positions from every warning in a group, deduplicated, in document order. */\r\nfunction mergeLocations(group: CSSWarning[]): SourceLocation[] {\r\n const byOffset = new Map<number, SourceLocation>();\r\n for (const w of group) {\r\n for (const loc of w.locs ?? (w.loc ? [w.loc] : [])) {\r\n if (!byOffset.has(loc.offset)) byOffset.set(loc.offset, loc);\r\n }\r\n }\r\n return [...byOffset.values()].sort((a, b) => a.offset - b.offset);\r\n}\r\n\r\n/**\r\n * Collapse per-client, per-selector warnings into one entry each.\r\n *\r\n * Grouped by property, severity **and message**: two clients can both drop a\r\n * property for different reasons, and merging those would attribute a caveat\r\n * to a client whose note never said it.\r\n */\r\nexport function collapseWarnings(warnings: CSSWarning[]): CollapsedFinding[] {\r\n const groups = new Map<string, CSSWarning[]>();\r\n for (const w of warnings) {\r\n // A NUL separator, because it cannot occur in a property, a severity or\r\n // a message; a space could, and two different triples would collide.\r\n const key = [w.property, w.severity, withoutClientName(w.message, w.client)].join(\"\\u0000\");\r\n const seen = groups.get(key);\r\n if (seen) seen.push(w);\r\n else groups.set(key, [w]);\r\n }\r\n\r\n return [...groups.values()].map((group) => {\r\n const first = group[0];\r\n const clients = [...new Set(group.map((w) => w.client))];\r\n const locs = mergeLocations(group);\r\n // A suggestion can differ per client (5 of 46 groups on the reference\r\n // fixture). Keeping the first would silently attribute one client's advice\r\n // to another, so a group that disagrees says nothing and lets the caller\r\n // ask `fix_email`.\r\n const suggestions = new Set(group.map((w) => w.suggestion ?? \"\"));\r\n const suggestion = suggestions.size === 1 ? first.suggestion : undefined;\r\n\r\n return {\r\n property: first.property,\r\n severity: first.severity,\r\n clients,\r\n message: withoutClientName(first.message, first.client),\r\n ...(suggestion ? { suggestion } : {}),\r\n ...(first.fixType ? { fixType: first.fixType } : {}),\r\n ...(group.some((w) => w.fix) ? { hasFix: true } : {}),\r\n ...(locs.length\r\n ? {\r\n loc: {\r\n line: locs[0].line,\r\n column: locs[0].column,\r\n offset: locs[0].offset,\r\n length: locs[0].length,\r\n },\r\n // Lines, not positions, and each line once: two occurrences on\r\n // one line are two offsets but one place to look.\r\n ...(() => {\r\n const lines = [...new Set(locs.slice(1).map((l) => l.line))].filter(\r\n (line) => line !== locs[0].line,\r\n );\r\n return lines.length ? { alsoAtLines: lines } : {};\r\n })(),\r\n }\r\n : {}),\r\n ...(group.some((w) => w.locsTruncated) ? { locsTruncated: true } : {}),\r\n };\r\n });\r\n}\r\n\r\n/** Keep only the warnings for these clients. An empty or absent list keeps all. */\r\nexport function forClients(warnings: CSSWarning[], clients?: string[]): CSSWarning[] {\r\n if (!clients?.length) return warnings;\r\n const wanted = new Set(clients);\r\n return warnings.filter((w) => wanted.has(w.client));\r\n}\r\n\r\n/** Every client id the engine knows, for validating a caller's filter. */\r\nexport function knownClientIds(): string[] {\r\n return EMAIL_CLIENTS.map((c) => c.id);\r\n}\r\n","import { compile } from \"@emailens/engine/compile\";\n\n/**\n * Getting a template to HTML before anything looks at it.\n *\n * The `format` parameter used to do nothing but choose the syntax of the fix\n * snippets, while the source was handed to the analyzer as-is. That is a quiet\n * and expensive lie: cheerio parses `<mjml><mj-section>…` without complaint,\n * finds no CSS in it, and the tools answer **zero findings**. An assistant\n * reads that as a clean bill of health and says the campaign is fine. A wrong\n * answer delivered confidently is worse than an error.\n *\n * So a non-HTML format is compiled first, with the same compilers the CLI uses.\n * They are optional peer dependencies of the engine (MJML alone is 56MB, and\n * this server is usually run through `npx`), so a caller can land on a machine\n * that has none of them. That case gets the engine's own \"install this\" message\n * plus the way out that needs nothing installed: compile it yourself and send\n * the HTML.\n */\n\nexport type SourceResult = { ok: true; html: string } | { ok: false; message: string };\n\nconst COMPILED: ReadonlySet<string> = new Set([\"jsx\", \"mjml\", \"maizzle\"]);\n\n/**\n * Is this \"the package isn't here\" rather than \"the template is wrong\"?\n *\n * The engine says `MJML compilation requires \"mjml\"` for a missing compiler\n * and `Sandbox strategy \"isolated-vm\" requires the \"isolated-vm\" package` when\n * the compiler is present but its sandbox is not, which is what React Email\n * hits first. Both are the same problem for the caller, so one pattern covers\n * them. Getting this wrong is cheap in one direction only: a misclassified\n * template error gains an irrelevant sentence, while a misclassified missing\n * package would tell someone to debug markup that is fine.\n */\nfunction isMissingCompiler(message: string): boolean {\n return /requires (?:the )?\"/.test(message);\n}\n\n/**\n * Compile `source` when `format` names a template language, or pass it through\n * when it is already HTML.\n */\nfunction isVueSfc(source: string): boolean {\n return /<template[\\s>]/i.test(source) && /<\\/template>/i.test(source);\n}\n\n/** An omitted format on a Vue SFC is maizzle. An explicit format is kept. */\nexport function resolveSourceFormat(source: string, format?: string): string {\n if (format) return format;\n return isVueSfc(source) ? \"maizzle\" : \"html\";\n}\n\nexport async function toHtml(source: string, format?: string): Promise<SourceResult> {\n const resolved = resolveSourceFormat(source, format);\n if (resolved === \"html\") return { ok: true, html: source };\n if (!COMPILED.has(resolved)) {\n return { ok: false, message: `Unknown format \"${resolved}\". Use html, jsx, mjml, or maizzle.` };\n }\n\n try {\n return { ok: true, html: await compile(source, resolved as \"jsx\" | \"mjml\" | \"maizzle\") };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (isMissingCompiler(message)) {\n return {\n ok: false,\n message:\n `${message}\\n\\nThat has to happen where this MCP server runs, which may not be somewhere ` +\n `you can install to. If it is not, compile the template yourself and send the resulting ` +\n `HTML with format \"html\".`,\n };\n }\n return { ok: false, message };\n }\n}\n","export const config = {\n get apiKey(): string | null {\n return process.env.EMAILENS_API_KEY ?? null;\n },\n get apiUrl(): string {\n return process.env.EMAILENS_API_URL ?? \"https://emailens.dev\";\n },\n get isHosted(): boolean {\n return this.apiKey !== null;\n },\n};\n","export interface ApiRequestOptions {\n apiKey: string;\n apiUrl: string;\n method?: \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\";\n body?: unknown;\n timeoutMs?: number;\n}\n\nexport interface ApiResult<T = unknown> {\n data?: T;\n isError?: true;\n error?: string;\n}\n\nexport async function apiRequest<T = unknown>(\n path: string,\n options: ApiRequestOptions,\n): Promise<ApiResult<T>> {\n const { apiKey, apiUrl, method = \"GET\", body, timeoutMs = 30_000 } = options;\n const url = `${apiUrl}${path}`;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: method !== \"GET\" ? method : body ? \"POST\" : \"GET\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n if (response.status === 401) {\n return {\n isError: true,\n error: \"Invalid or revoked API key. Generate a new one at emailens.dev/settings/api-keys?ref=mcp\",\n };\n }\n\n if (response.status === 403) {\n const data = (await response.json().catch(() => ({}))) as Record<string, unknown>;\n return {\n isError: true,\n error: data.upgrade\n ? \"This feature requires a plan upgrade. Visit emailens.dev/pricing?ref=mcp\"\n : \"Access denied. Check your plan at emailens.dev/pricing?ref=mcp\",\n };\n }\n\n if (response.status === 429) {\n const data = (await response.json().catch(() => ({}))) as Record<string, number>;\n return {\n isError: true,\n error: `Daily quota exceeded${data.limit ? ` (${data.limit - (data.remaining ?? 0)}/${data.limit})` : \"\"}. Resets tomorrow. Upgrade at emailens.dev/pricing?ref=mcp`,\n };\n }\n\n if (!response.ok) {\n const data = (await response.json().catch(() => ({}))) as Record<string, unknown>;\n return { isError: true, error: (data.error as string) ?? `API error: ${response.status}` };\n }\n\n const data = (await response.json()) as T;\n return { data };\n } catch (err) {\n if ((err as Error).name === \"AbortError\") {\n return { isError: true, error: `Request timed out after ${timeoutMs / 1000}s. Try again or check emailens.dev status.` };\n }\n return { isError: true, error: `Network error: ${(err as Error).message}` };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function mcpError(message: string) {\n return {\n content: [{ type: \"text\" as const, text: message }],\n isError: true,\n };\n}\n\nexport function noApiKeyError(toolName: string, extraInfo?: string) {\n const lines = [\n `${toolName} requires an Emailens API key.`,\n \"\",\n \"1. Sign up free at emailens.dev?ref=mcp\",\n \"2. Create a key at emailens.dev/settings/api-keys?ref=mcp\",\n \"3. Add to your MCP config:\",\n \"\",\n ' \"env\": { \"EMAILENS_API_KEY\": \"ek_live_...\" }',\n ];\n if (extraInfo) {\n lines.push(\"\", extraInfo);\n } else {\n lines.push(\"\", \"Free plan includes 30 previews/day (each preview can include screenshots).\");\n }\n return mcpError(lines.join(\"\\n\"));\n}\n"],"mappings":"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","names":["EMAIL_CLIENTS","data","EMAIL_CLIENTS"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@emailens/mcp",
3
3
  "mcpName": "io.github.emailens/mcp",
4
- "version": "0.7.1",
4
+ "version": "0.9.1",
5
5
  "description": "MCP server for email compatibility analysis: preview, analyze, fix, diff and capture screenshots across 21 email clients, from HTML, MJML, Maizzle or React Email sources, via Claude or any MCP-compatible tool.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -28,15 +28,15 @@
28
28
  "prepublishOnly": "bun run build"
29
29
  },
30
30
  "dependencies": {
31
- "@emailens/engine": "^0.11.1",
32
- "@modelcontextprotocol/sdk": "^1.12.0",
33
- "zod": "^3.24.0"
31
+ "@emailens/engine": "^0.14.2",
32
+ "@modelcontextprotocol/sdk": "^1.30.0",
33
+ "zod": "^3.25.76"
34
34
  },
35
35
  "devDependencies": {
36
- "@types/node": "^25.3.0",
37
- "bun-types": "^1.3.9",
36
+ "@types/node": "^25.9.5",
37
+ "bun-types": "^1.4.0",
38
38
  "mjml": "^5.4.0",
39
- "tsup": "^8.4.0",
39
+ "tsup": "^8.5.1",
40
40
  "typescript": "^5.9.3"
41
41
  },
42
42
  "keywords": [