@emailens/mcp 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  </div>
18
18
 
19
- MCP server for email compatibility analysis. Analyze, preview, diff, and fix emails across 21 email clients — plus capture real screenshots and create shareable links with an optional API key.
19
+ MCP server for email compatibility analysis. Analyze, preview, diff, and fix emails across 21 email clients, plus capture real screenshots and create shareable links with an optional API key.
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
 
@@ -53,7 +53,7 @@ Add to `claude_desktop_config.json`:
53
53
  claude mcp add emailens -- npx -y @emailens/mcp
54
54
  ```
55
55
 
56
- ### With API Key (optional — unlocks screenshots + sharing)
56
+ ### With API Key (optional, unlocks screenshots + sharing)
57
57
 
58
58
  ```json
59
59
  {
@@ -73,7 +73,7 @@ Get your free API key at [emailens.dev/settings/api-keys](https://emailens.dev/s
73
73
 
74
74
  ### Remote (no install)
75
75
 
76
- Use the hosted endpoint — no npm or Node.js needed. API key required.
76
+ Use the hosted endpoint: no npm or Node.js needed. API key required.
77
77
 
78
78
  ```json
79
79
  {
@@ -94,7 +94,7 @@ Use the hosted endpoint — no npm or Node.js needed. API key required.
94
94
 
95
95
  #### `preview_email`
96
96
 
97
- Full email compatibility preview — transforms HTML for 21 clients, analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.
97
+ Full email compatibility preview: transforms HTML for 21 clients, analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.
98
98
 
99
99
  | Parameter | Type | Required | Description |
100
100
  |---|---|---|---|
@@ -104,7 +104,7 @@ Full email compatibility preview — transforms HTML for 21 clients, analyzes CS
104
104
 
105
105
  #### `analyze_email`
106
106
 
107
- Quick CSS compatibility analysis — returns per-client scores and one finding per problem. Faster than `audit_email` when you only need CSS compatibility.
107
+ Quick CSS compatibility analysis; returns per-client scores and one finding per problem. Faster than `audit_email` when you only need CSS compatibility.
108
108
 
109
109
  | Parameter | Type | Required | Description |
110
110
  |---|---|---|---|
@@ -115,8 +115,8 @@ Quick CSS compatibility analysis — returns per-client scores and one finding p
115
115
 
116
116
  **One finding per problem, not one per client.** The engine reports per client
117
117
  because a score is per client, and per selector because a fix is per selector.
118
- On an ordinary newsletter `border-radius` arrives twelve times — two clients
119
- that drop it, six selectors that use it — with the same sentence in every copy.
118
+ On an ordinary newsletter `border-radius` arrives twelve times (two clients
119
+ that drop it, six selectors that use it) with the same sentence in every copy.
120
120
  That was 286KB of JSON, about 73,000 tokens, for an 11KB email.
121
121
 
122
122
  Findings group by property, severity and message, listing the clients affected,
@@ -135,19 +135,19 @@ with positions merged across all of them. The same email now returns 40KB.
135
135
  }
136
136
  ```
137
137
 
138
- Fix snippets are not included — they were 93KB of that 286KB, and `fix_email`
138
+ Fix snippets are not included; they were 93KB of that 286KB, and `fix_email`
139
139
  produces them for the issues you decide to act on. `hasFix` tells you one is
140
140
  available; `fixType` tells you whether the repair is markup or CSS.
141
141
 
142
142
  Pass `detail: "full"` for the engine's per-client shape with snippets, and
143
- `clients: ["gmail-web", "outlook-windows"]` to report only what you care about
144
- — the fastest further saving, roughly halving the response for two clients.
143
+ `clients: ["gmail-web", "outlook-windows"]` to report only what you care about:
144
+ the fastest further saving, roughly halving the response for two clients.
145
145
  Scores stay whole-email either way: narrowing the report does not change what
146
- the email is worth elsewhere. An unknown client ID is rejected by name — an
146
+ the email is worth elsewhere. An unknown client ID is rejected by name; an
147
147
  empty result would read as "this email is fine for that client".
148
148
 
149
- **Source positions.** For HTML input, every warning carries `loc` — `line`,
150
- `column`, `offset`, `length` — for the first occurrence, plus `alsoAtLines` for
149
+ **Source positions.** For HTML input, every warning carries `loc` (`line`,
150
+ `column`, `offset`, `length`) for the first occurrence, plus `alsoAtLines` for
151
151
  any others, so an assistant can edit the exact source and knows where the rest
152
152
  are. `audit_email` positions its other findings the same way.
153
153
 
@@ -161,17 +161,19 @@ are. `audit_email` positions its other findings the same way.
161
161
 
162
162
  Later occurrences are line numbers rather than full positions on purpose: this
163
163
  response is read by a model paying for every token, and a real newsletter can
164
- produce over a thousand occurrences — carrying them all in full nearly doubles
164
+ produce over a thousand occurrences; carrying them all in full nearly doubles
165
165
  the payload.
166
166
 
167
167
  Positions are reported for `html` input only. JSX, MJML and Maizzle are compiled
168
168
  before analysis, so a line number would point into generated output rather than
169
- the file you have open — the tools omit it instead of returning one that looks
169
+ the file you have open: the tools omit it instead of returning one that looks
170
170
  authoritative.
171
171
 
172
172
  #### `audit_email`
173
173
 
174
- Comprehensive quality audit — CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, and visual bugs.
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
+
176
+ 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.
175
177
 
176
178
  | Parameter | Type | Required | Description |
177
179
  |---|---|---|---|
@@ -198,7 +200,7 @@ List all 21 supported email clients with IDs, names, engines, and dark mode supp
198
200
 
199
201
  #### `diff_emails`
200
202
 
201
- Compare two email HTML versions — shows score changes, fixed issues, and introduced issues per client.
203
+ Compare two email HTML versions; shows score changes, fixed issues, and introduced issues per client.
202
204
 
203
205
  | Parameter | Type | Required | Description |
204
206
  |---|---|---|---|
@@ -208,7 +210,7 @@ Compare two email HTML versions — shows score changes, fixed issues, and intro
208
210
 
209
211
  #### `check_deliverability`
210
212
 
211
- Check email deliverability for a domain — SPF, DKIM, DMARC, MX, BIMI records with a score and actionable issues.
213
+ Check email deliverability for a domain: SPF, DKIM, DMARC, MX, BIMI records with a score and actionable issues.
212
214
 
213
215
  | Parameter | Type | Required | Description |
214
216
  |---|---|---|---|
@@ -265,7 +267,7 @@ npm install sucrase react @react-email/components @react-email/render # React E
265
267
 
266
268
  They have to be installed where the server runs, which is not always somewhere
267
269
  you control. If it is not, compile the template yourself and send the resulting
268
- HTML with `format: "html"` — the tools say so when they hit this.
270
+ HTML with `format: "html"`: the tools say so when they hit this.
269
271
 
270
272
  ## Supported Email Clients (21)
271
273
 
@@ -293,6 +295,13 @@ HTML with `format: "html"` — the tools say so when they hit this.
293
295
  | Fastmail | `fastmail` | Yes | New in v0.10.0 |
294
296
  | Superhuman | `superhuman` | Yes | |
295
297
 
298
+ ## Releasing
299
+
300
+ Two publishes: npm, and the MCP registry. The registry listing sat five
301
+ releases behind because nothing pushed `server.json`, so that half is a
302
+ workflow now. [RELEASING.md](./RELEASING.md) has the details and the four
303
+ places the version has to agree.
304
+
296
305
  ## Development
297
306
 
298
307
  ```bash
package/dist/index.js CHANGED
@@ -211,7 +211,7 @@ function toFramework(format) {
211
211
  }
212
212
  var formatEnum = z.enum(["html", "jsx", "mjml", "maizzle"]).optional();
213
213
  var detailEnum = z.enum(["summary", "full"]).optional().describe(
214
- "'summary' (default) returns one entry per problem, listing the clients it affects \u2014 roughly 10x smaller. 'full' returns one entry per client with fix snippets."
214
+ "'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
215
  );
216
216
  var clientsParam = z.array(z.string()).optional().describe(
217
217
  "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."
@@ -270,13 +270,13 @@ function validateHtmlSize(html) {
270
270
  }
271
271
  var server = new McpServer({
272
272
  name: "emailens",
273
- version: "0.6.2"
273
+ version: "0.7.0"
274
274
  });
275
275
  server.registerTool(
276
276
  "preview_email",
277
277
  {
278
278
  title: "Preview Email",
279
- description: `Full email compatibility preview \u2014 transforms HTML for ${EMAIL_CLIENTS2.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.`,
279
+ description: `Full email compatibility preview, transforms HTML for ${EMAIL_CLIENTS2.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.`,
280
280
  inputSchema: {
281
281
  html: z.string().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so"),
282
282
  clients: z.array(z.string()).optional().describe("Optional client ID filter (e.g. ['gmail-web', 'outlook-windows'])"),
@@ -355,7 +355,7 @@ server.registerTool(
355
355
  "analyze_email",
356
356
  {
357
357
  title: "Analyze Email",
358
- description: "Quick CSS compatibility analysis \u2014 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.).",
358
+ 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
359
  inputSchema: {
360
360
  html: z.string().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so"),
361
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."),
@@ -405,11 +405,11 @@ server.registerTool(
405
405
  "audit_email",
406
406
  {
407
407
  title: "Audit Email",
408
- description: "Comprehensive email quality audit \u2014 CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), and template variables. 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.",
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, and design consistency. 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.",
409
409
  inputSchema: {
410
410
  html: z.string().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so"),
411
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"])).optional().describe("Checks to skip (e.g. ['spam', 'images'])"),
412
+ skip: z.array(z.enum(["spam", "links", "accessibility", "images", "compatibility", "inboxPreview", "size", "templateVariables", "overflow", "visual", "darkContrast", "mobileContrast", "design"])).optional().describe("Checks to skip (e.g. ['spam', 'images'])"),
413
413
  detail: detailEnum,
414
414
  clients: clientsParam
415
415
  },
@@ -454,7 +454,10 @@ server.registerTool(
454
454
  size: report.size,
455
455
  templateVariables: report.templateVariables,
456
456
  overflow: report.overflow,
457
- visual: report.visual
457
+ visual: report.visual,
458
+ darkContrast: report.darkContrast,
459
+ mobileContrast: report.mobileContrast,
460
+ design: report.design
458
461
  },
459
462
  null,
460
463
  2
@@ -470,7 +473,7 @@ server.registerTool(
470
473
  title: "Fix Email",
471
474
  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.",
472
475
  inputSchema: {
473
- html: z.string().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so \u2014 the source to fix"),
476
+ html: z.string().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so: the source to fix"),
474
477
  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."),
475
478
  scope: z.enum(["all", "current"]).optional().describe("'all' (default) or 'current' (requires selectedClientId)"),
476
479
  selectedClientId: z.string().optional().describe("Client ID to scope fixes to (e.g. 'outlook-windows')")
@@ -584,7 +587,7 @@ server.registerTool(
584
587
  "diff_emails",
585
588
  {
586
589
  title: "Diff Emails",
587
- description: "Compare two email HTML versions \u2014 shows score changes, fixed issues, and newly introduced issues per client. Use after making fixes to verify improvements.",
590
+ description: "Compare two email HTML versions, shows score changes, fixed issues, and newly introduced issues per client. Use after making fixes to verify improvements.",
588
591
  inputSchema: {
589
592
  before: z.string().describe("Original email source"),
590
593
  after: z.string().describe("Modified email source, in the same format"),
@@ -661,7 +664,7 @@ server.registerTool(
661
664
  "check_deliverability",
662
665
  {
663
666
  title: "Check Deliverability",
664
- description: "Check email deliverability for a domain \u2014 SPF, DKIM, DMARC, MX, and BIMI records. Returns a score (0-100) and actionable issues. Uses DNS lookups (no API key needed).",
667
+ description: "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).",
665
668
  inputSchema: {
666
669
  domain: z.string().min(3).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, "Invalid domain format").describe("Domain to check (e.g. 'company.com')")
667
670
  },
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.6.2\",\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), and template variables. 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\"]))\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 },\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\";\r\n\r\n/**\r\n * Getting a template to HTML before anything looks at it.\r\n *\r\n * The `format` parameter used to do nothing but choose the syntax of the fix\r\n * snippets, while the source was handed to the analyzer as-is. That is a quiet\r\n * and expensive lie: cheerio parses `<mjml><mj-section>…` without complaint,\r\n * finds no CSS in it, and the tools answer **zero findings**. An assistant\r\n * reads that as a clean bill of health and says the campaign is fine. A wrong\r\n * answer delivered confidently is worse than an error.\r\n *\r\n * So a non-HTML format is compiled first, with the same compilers the CLI uses.\r\n * They are optional peer dependencies of the engine — MJML alone is 56MB, and\r\n * this server is usually run through `npx` — so a caller can land on a machine\r\n * that has none of them. That case gets the engine's own \"install this\" message\r\n * plus the way out that needs nothing installed: compile it yourself and send\r\n * the HTML.\r\n */\r\n\r\nexport type SourceResult = { ok: true; html: string } | { ok: false; message: string };\r\n\r\nconst COMPILED: ReadonlySet<string> = new Set([\"jsx\", \"mjml\", \"maizzle\"]);\r\n\r\n/**\r\n * Is this \"the package isn't here\" rather than \"the template is wrong\"?\r\n *\r\n * The engine reports every missing peer with the same sentence — `MJML\r\n * compilation requires \"mjml\"` — across all three compilers, so one pattern\r\n * covers them. Getting this wrong is cheap in one direction only: a\r\n * misclassified template error gains an irrelevant sentence, while a\r\n * misclassified missing package would tell someone to debug markup that is\r\n * fine.\r\n */\r\nfunction isMissingCompiler(message: string): boolean {\r\n return /compilation requires \"/.test(message);\r\n}\r\n\r\n/**\r\n * Compile `source` when `format` names a template language, or pass it through\r\n * when it is already HTML.\r\n */\r\nexport async function toHtml(source: string, format?: string): Promise<SourceResult> {\r\n if (!format || format === \"html\") return { ok: true, html: source };\r\n if (!COMPILED.has(format)) {\r\n return { ok: false, message: `Unknown format \"${format}\". Use html, jsx, mjml, or maizzle.` };\r\n }\r\n\r\n try {\r\n return { ok: true, html: await compile(source, format as \"jsx\" | \"mjml\" | \"maizzle\") };\r\n } catch (err) {\r\n const message = err instanceof Error ? err.message : String(err);\r\n if (isMissingCompiler(message)) {\r\n return {\r\n ok: false,\r\n message:\r\n `${message}\\n\\nThat has to happen where this MCP server runs, which may not be somewhere ` +\r\n `you can install to. If it is not, compile the template yourself and send the resulting ` +\r\n `HTML with format \"html\".`,\r\n };\r\n }\r\n return { ok: false, message };\r\n }\r\n}\r\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\";\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, and design consistency. 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\"]))\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 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\";\r\n\r\n/**\r\n * Getting a template to HTML before anything looks at it.\r\n *\r\n * The `format` parameter used to do nothing but choose the syntax of the fix\r\n * snippets, while the source was handed to the analyzer as-is. That is a quiet\r\n * and expensive lie: cheerio parses `<mjml><mj-section>…` without complaint,\r\n * finds no CSS in it, and the tools answer **zero findings**. An assistant\r\n * reads that as a clean bill of health and says the campaign is fine. A wrong\r\n * answer delivered confidently is worse than an error.\r\n *\r\n * So a non-HTML format is compiled first, with the same compilers the CLI uses.\r\n * They are optional peer dependencies of the engine (MJML alone is 56MB, and\r\n * this server is usually run through `npx`), so a caller can land on a machine\r\n * that has none of them. That case gets the engine's own \"install this\" message\r\n * plus the way out that needs nothing installed: compile it yourself and send\r\n * the HTML.\r\n */\r\n\r\nexport type SourceResult = { ok: true; html: string } | { ok: false; message: string };\r\n\r\nconst COMPILED: ReadonlySet<string> = new Set([\"jsx\", \"mjml\", \"maizzle\"]);\r\n\r\n/**\r\n * Is this \"the package isn't here\" rather than \"the template is wrong\"?\r\n *\r\n * The engine reports every missing peer with the same sentence, `MJML\r\n * compilation requires \"mjml\"`, across all three compilers, so one pattern\r\n * covers them. Getting this wrong is cheap in one direction only: a\r\n * misclassified template error gains an irrelevant sentence, while a\r\n * misclassified missing package would tell someone to debug markup that is\r\n * fine.\r\n */\r\nfunction isMissingCompiler(message: string): boolean {\r\n return /compilation requires \"/.test(message);\r\n}\r\n\r\n/**\r\n * Compile `source` when `format` names a template language, or pass it through\r\n * when it is already HTML.\r\n */\r\nexport async function toHtml(source: string, format?: string): Promise<SourceResult> {\r\n if (!format || format === \"html\") return { ok: true, html: source };\r\n if (!COMPILED.has(format)) {\r\n return { ok: false, message: `Unknown format \"${format}\". Use html, jsx, mjml, or maizzle.` };\r\n }\r\n\r\n try {\r\n return { ok: true, html: await compile(source, format as \"jsx\" | \"mjml\" | \"maizzle\") };\r\n } catch (err) {\r\n const message = err instanceof Error ? err.message : String(err);\r\n if (isMissingCompiler(message)) {\r\n return {\r\n ok: false,\r\n message:\r\n `${message}\\n\\nThat has to happen where this MCP server runs, which may not be somewhere ` +\r\n `you can install to. If it is not, compile the template yourself and send the resulting ` +\r\n `HTML with format \"html\".`,\r\n };\r\n }\r\n return { ok: false, message };\r\n }\r\n}\r\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.6.2",
4
+ "version": "0.7.0",
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,7 +28,7 @@
28
28
  "prepublishOnly": "bun run build"
29
29
  },
30
30
  "dependencies": {
31
- "@emailens/engine": "^0.10.4",
31
+ "@emailens/engine": "^0.11.0",
32
32
  "@modelcontextprotocol/sdk": "^1.12.0",
33
33
  "zod": "^3.24.0"
34
34
  },