@emailens/mcp 0.12.0 → 0.13.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 +39 -30
- package/dist/index.js +154 -57
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://www.npmjs.com/package/@emailens/mcp)
|
|
12
12
|
[](./LICENSE)
|
|
13
13
|
[](https://registry.modelcontextprotocol.io)
|
|
14
|
-
[](https://smithery.ai/servers/emailens-mcp/emailens-mcp)
|
|
15
15
|
[](https://github.com/emailens/mcp/stargazers)
|
|
16
16
|
|
|
17
17
|
</div>
|
|
@@ -98,7 +98,8 @@ Full email compatibility preview: transforms HTML for 21 clients, analyzes CSS,
|
|
|
98
98
|
|
|
99
99
|
| Parameter | Type | Required | Description |
|
|
100
100
|
|---|---|---|---|
|
|
101
|
-
| `html` | string |
|
|
101
|
+
| `html` | string | One of | Email HTML source |
|
|
102
|
+
| `path` | string | One of | Absolute path to the source file instead of `html` (`.html`, `.htm`, `.mjml`, `.jsx`, `.tsx`, `.vue`); the extension sets `format` |
|
|
102
103
|
| `clients` | string[] | No | Filter to specific client IDs |
|
|
103
104
|
| `format` | enum | No | `"html"`, `"jsx"`, `"mjml"`, `"maizzle"` |
|
|
104
105
|
|
|
@@ -108,7 +109,8 @@ Quick CSS compatibility analysis; returns per-client scores and one finding per
|
|
|
108
109
|
|
|
109
110
|
| Parameter | Type | Required | Description |
|
|
110
111
|
|---|---|---|---|
|
|
111
|
-
| `html` | string |
|
|
112
|
+
| `html` | string | One of | Email HTML source |
|
|
113
|
+
| `path` | string | One of | Absolute path to the source file instead of `html` (`.html`, `.htm`, `.mjml`, `.jsx`, `.tsx`, `.vue`); the extension sets `format` |
|
|
112
114
|
| `format` | enum | No | Input format |
|
|
113
115
|
| `detail` | enum | No | `"summary"` (default) or `"full"` |
|
|
114
116
|
| `clients` | string[] | No | Only report these client IDs |
|
|
@@ -178,13 +180,15 @@ The last three cover what a light desktop preview cannot show: text that disappe
|
|
|
178
180
|
|
|
179
181
|
| Parameter | Type | Required | Description |
|
|
180
182
|
|---|---|---|---|
|
|
181
|
-
| `html` | string |
|
|
183
|
+
| `html` | string | One of | Email HTML source |
|
|
184
|
+
| `path` | string | One of | Absolute path to the source file instead of `html` (`.html`, `.htm`, `.mjml`, `.jsx`, `.tsx`, `.vue`); the extension sets `format` |
|
|
182
185
|
| `format` | enum | No | Input format |
|
|
183
186
|
| `detail` | enum | No | `"summary"` (default) or `"full"` |
|
|
184
187
|
| `clients` | string[] | No | Only report these client IDs |
|
|
185
188
|
| `skip` | string[] | No | Checks to skip (e.g. `["spam", "images"]`) |
|
|
186
189
|
| `targetingPolicy` | enum | No | `"progressive"` (default), `"strict"`, or `"lenient"` |
|
|
187
190
|
| `emailType` | enum | No | `"transactional"` (no unsubscribe link required) or `"marketing"`. Omitted: guessed from the content |
|
|
191
|
+
| `previousHtml` | string | No | The design to hold this version to, usually your first draft. Omitted: the first version of this email audited in the session. What the edits removed comes back as `designRemovals` |
|
|
188
192
|
|
|
189
193
|
Fix `error` and `warning` findings. `info` findings are not problems: the email falls back safely there, so never remove responsive (`@media`) or dark-mode CSS to clear them.
|
|
190
194
|
|
|
@@ -194,7 +198,8 @@ Generate a structured fix prompt for compatibility issues. Returns markdown with
|
|
|
194
198
|
|
|
195
199
|
| Parameter | Type | Required | Description |
|
|
196
200
|
|---|---|---|---|
|
|
197
|
-
| `html` | string |
|
|
201
|
+
| `html` | string | One of | Email HTML to fix |
|
|
202
|
+
| `path` | string | One of | Absolute path to the source file instead of `html` (`.html`, `.htm`, `.mjml`, `.jsx`, `.tsx`, `.vue`); the extension sets `format` |
|
|
198
203
|
| `format` | enum | No | Controls fix syntax |
|
|
199
204
|
| `scope` | enum | No | `"all"` or `"current"` |
|
|
200
205
|
| `selectedClientId` | string | No | Client ID for scoped fixes |
|
|
@@ -209,8 +214,8 @@ Compare two email HTML versions; shows score changes, fixed issues, and introduc
|
|
|
209
214
|
|
|
210
215
|
| Parameter | Type | Required | Description |
|
|
211
216
|
|---|---|---|---|
|
|
212
|
-
| `before` | string |
|
|
213
|
-
| `after` | string |
|
|
217
|
+
| `before` / `beforePath` | string | One of | Original email source, or its file |
|
|
218
|
+
| `after` / `afterPath` | string | One of | Modified email source, or its file |
|
|
214
219
|
| `format` | enum | No | Input format |
|
|
215
220
|
|
|
216
221
|
#### `check_deliverability`
|
|
@@ -229,7 +234,8 @@ Capture real email screenshots across 21 clients in real browsers. Screenshots a
|
|
|
229
234
|
|
|
230
235
|
| Parameter | Type | Required | Description |
|
|
231
236
|
|---|---|---|---|
|
|
232
|
-
| `html` | string |
|
|
237
|
+
| `html` | string | One of | Email HTML source |
|
|
238
|
+
| `path` | string | One of | Absolute path to the source file instead of `html` (`.html`, `.htm`, `.mjml`, `.jsx`, `.tsx`, `.vue`); the extension sets `format` |
|
|
233
239
|
| `format` | enum | No | Input format |
|
|
234
240
|
| `clients` | string[] | No | Filter clients |
|
|
235
241
|
| `modes` | string[] | No | `["light"]`, `["dark"]`, or `["light", "dark"]` |
|
|
@@ -243,7 +249,8 @@ Create a shareable link. Recipients see the full analysis without an account.
|
|
|
243
249
|
|
|
244
250
|
| Parameter | Type | Required | Description |
|
|
245
251
|
|---|---|---|---|
|
|
246
|
-
| `html` | string |
|
|
252
|
+
| `html` | string | One of | Email HTML source |
|
|
253
|
+
| `path` | string | One of | Absolute path to the source file instead of `html` (`.html`, `.htm`, `.mjml`, `.jsx`, `.tsx`, `.vue`); the extension sets `format` |
|
|
247
254
|
| `title` | string | No | Display title |
|
|
248
255
|
| `format` | enum | No | Input format |
|
|
249
256
|
|
|
@@ -278,29 +285,31 @@ HTML with `format: "html"`: the tools say so when they hit this.
|
|
|
278
285
|
|
|
279
286
|
## Supported Email Clients (21)
|
|
280
287
|
|
|
288
|
+
Each client links to its CSS support page. Setup guides for every assistant: [emailens.dev/mcp](https://emailens.dev/mcp).
|
|
289
|
+
|
|
281
290
|
| Client | ID | Dark Mode | Notes |
|
|
282
291
|
|---|---|---|---|
|
|
283
|
-
| Gmail | `gmail-web` | Yes | |
|
|
284
|
-
| Gmail Android | `gmail-android` | Yes | |
|
|
285
|
-
| Gmail iOS | `gmail-ios` | Yes | |
|
|
286
|
-
| Outlook 365 | `outlook-web` | Yes | |
|
|
287
|
-
| Outlook Windows | `outlook-windows` | No | |
|
|
288
|
-
| Outlook Windows Legacy | `outlook-windows-legacy` | No | Deprecated Oct 2026 |
|
|
289
|
-
| Outlook iOS | `outlook-ios` | Yes | New in v0.4.0 |
|
|
290
|
-
| Outlook Android | `outlook-android` | Yes | New in v0.4.0 |
|
|
291
|
-
| Outlook for Mac | `outlook-macos` | Yes | New in v0.10.0 |
|
|
292
|
-
| Apple Mail | `apple-mail-macos` | Yes | |
|
|
293
|
-
| Apple Mail iOS | `apple-mail-ios` | Yes | |
|
|
294
|
-
| Yahoo Mail | `yahoo-mail` | Yes | |
|
|
295
|
-
| Yahoo Mail Android | `yahoo-mail-android` | Yes | New in v0.10.0 |
|
|
296
|
-
| Yahoo Mail iOS | `yahoo-mail-ios` | Yes | New in v0.10.0 |
|
|
297
|
-
| Samsung Mail | `samsung-mail` | Yes | |
|
|
298
|
-
| Thunderbird | `thunderbird` | No | |
|
|
299
|
-
| HEY Mail | `hey-mail` | Yes | |
|
|
300
|
-
| Proton Mail | `protonmail` | Yes | New in v0.10.0 |
|
|
301
|
-
| AOL Mail | `aol` | Yes | New in v0.10.0 |
|
|
302
|
-
| Fastmail | `fastmail` | Yes | New in v0.10.0 |
|
|
303
|
-
| Superhuman | `superhuman` | Yes | |
|
|
292
|
+
| [Gmail](https://emailens.dev/email-css/clients/gmail-web) | `gmail-web` | Yes | |
|
|
293
|
+
| [Gmail Android](https://emailens.dev/email-css/clients/gmail-android) | `gmail-android` | Yes | |
|
|
294
|
+
| [Gmail iOS](https://emailens.dev/email-css/clients/gmail-ios) | `gmail-ios` | Yes | |
|
|
295
|
+
| [Outlook 365](https://emailens.dev/email-css/clients/outlook-web) | `outlook-web` | Yes | |
|
|
296
|
+
| [Outlook Windows](https://emailens.dev/email-css/clients/outlook-windows) | `outlook-windows` | No | |
|
|
297
|
+
| [Outlook Windows Legacy](https://emailens.dev/email-css/clients/outlook-windows-legacy) | `outlook-windows-legacy` | No | Deprecated Oct 2026 |
|
|
298
|
+
| [Outlook iOS](https://emailens.dev/email-css/clients/outlook-ios) | `outlook-ios` | Yes | New in v0.4.0 |
|
|
299
|
+
| [Outlook Android](https://emailens.dev/email-css/clients/outlook-android) | `outlook-android` | Yes | New in v0.4.0 |
|
|
300
|
+
| [Outlook for Mac](https://emailens.dev/email-css/clients/outlook-macos) | `outlook-macos` | Yes | New in v0.10.0 |
|
|
301
|
+
| [Apple Mail](https://emailens.dev/email-css/clients/apple-mail-macos) | `apple-mail-macos` | Yes | |
|
|
302
|
+
| [Apple Mail iOS](https://emailens.dev/email-css/clients/apple-mail-ios) | `apple-mail-ios` | Yes | |
|
|
303
|
+
| [Yahoo Mail](https://emailens.dev/email-css/clients/yahoo-mail) | `yahoo-mail` | Yes | |
|
|
304
|
+
| [Yahoo Mail Android](https://emailens.dev/email-css/clients/yahoo-mail-android) | `yahoo-mail-android` | Yes | New in v0.10.0 |
|
|
305
|
+
| [Yahoo Mail iOS](https://emailens.dev/email-css/clients/yahoo-mail-ios) | `yahoo-mail-ios` | Yes | New in v0.10.0 |
|
|
306
|
+
| [Samsung Mail](https://emailens.dev/email-css/clients/samsung-mail) | `samsung-mail` | Yes | |
|
|
307
|
+
| [Thunderbird](https://emailens.dev/email-css/clients/thunderbird) | `thunderbird` | No | |
|
|
308
|
+
| [HEY Mail](https://emailens.dev/email-css/clients/hey-mail) | `hey-mail` | Yes | |
|
|
309
|
+
| [Proton Mail](https://emailens.dev/email-css/clients/protonmail) | `protonmail` | Yes | New in v0.10.0 |
|
|
310
|
+
| [AOL Mail](https://emailens.dev/email-css/clients/aol) | `aol` | Yes | New in v0.10.0 |
|
|
311
|
+
| [Fastmail](https://emailens.dev/email-css/clients/fastmail) | `fastmail` | Yes | New in v0.10.0 |
|
|
312
|
+
| [Superhuman](https://emailens.dev/email-css/clients/superhuman) | `superhuman` | Yes | |
|
|
304
313
|
|
|
305
314
|
## Releasing
|
|
306
315
|
|
package/dist/index.js
CHANGED
|
@@ -121,8 +121,9 @@ import {
|
|
|
121
121
|
estimateAiFixTokens,
|
|
122
122
|
diffResults,
|
|
123
123
|
toPlainText,
|
|
124
|
+
checkDesignRetention,
|
|
124
125
|
EMAIL_CLIENTS as EMAIL_CLIENTS2,
|
|
125
|
-
MAX_HTML_SIZE
|
|
126
|
+
MAX_HTML_SIZE as MAX_HTML_SIZE2
|
|
126
127
|
} from "@emailens/engine";
|
|
127
128
|
|
|
128
129
|
// src/config.ts
|
|
@@ -138,6 +139,74 @@ var config = {
|
|
|
138
139
|
}
|
|
139
140
|
};
|
|
140
141
|
|
|
142
|
+
// src/drafts.ts
|
|
143
|
+
var MAX_DRAFTS = 200;
|
|
144
|
+
var SAME_EMAIL = 0.7;
|
|
145
|
+
var drafts = /* @__PURE__ */ new Map();
|
|
146
|
+
function title(html) {
|
|
147
|
+
return (html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? "").trim().toLowerCase();
|
|
148
|
+
}
|
|
149
|
+
function words(html) {
|
|
150
|
+
const text = html.replace(/<!--[\s\S]*?-->/g, " ").replace(/<(style|script|head)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]+>/g, " ").toLowerCase();
|
|
151
|
+
return new Set(text.match(/[\p{L}\p{N}]{3,}/gu) ?? []);
|
|
152
|
+
}
|
|
153
|
+
function overlap(a, b) {
|
|
154
|
+
if (!a.size || !b.size) return 0;
|
|
155
|
+
let shared = 0;
|
|
156
|
+
for (const w of a) if (b.has(w)) shared++;
|
|
157
|
+
return shared / (a.size + b.size - shared);
|
|
158
|
+
}
|
|
159
|
+
function firstDraft(html) {
|
|
160
|
+
const key = title(html);
|
|
161
|
+
const seen = words(html);
|
|
162
|
+
const list = drafts.get(key) ?? [];
|
|
163
|
+
const best = list.map((d) => ({ d, score: overlap(d.words, seen) })).sort((a, b) => b.score - a.score)[0];
|
|
164
|
+
if (best && best.score >= SAME_EMAIL) return best.d.html;
|
|
165
|
+
if ([...drafts.values()].reduce((n, l) => n + l.length, 0) >= MAX_DRAFTS) drafts.clear();
|
|
166
|
+
drafts.set(key, [...list, { words: seen, html }]);
|
|
167
|
+
return html;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/source.ts
|
|
171
|
+
import { readFile, realpath, stat } from "fs/promises";
|
|
172
|
+
import { extname } from "path";
|
|
173
|
+
import { MAX_HTML_SIZE } from "@emailens/engine";
|
|
174
|
+
var EXTENSIONS = {
|
|
175
|
+
".html": void 0,
|
|
176
|
+
".htm": void 0,
|
|
177
|
+
".mjml": "mjml",
|
|
178
|
+
".jsx": "jsx",
|
|
179
|
+
".tsx": "jsx",
|
|
180
|
+
".vue": "maizzle"
|
|
181
|
+
};
|
|
182
|
+
async function readSource(html, path, format, names = { html: "html", path: "path" }) {
|
|
183
|
+
if (html === void 0 === (path === void 0)) {
|
|
184
|
+
return { ok: false, message: `Pass exactly one of \`${names.html}\` (the source) or \`${names.path}\` (a file holding it).` };
|
|
185
|
+
}
|
|
186
|
+
if (html !== void 0) return { ok: true, html, format };
|
|
187
|
+
let real;
|
|
188
|
+
try {
|
|
189
|
+
real = await realpath(path);
|
|
190
|
+
} catch {
|
|
191
|
+
return { ok: false, message: `No file at ${path}. Pass an absolute path, or the source itself as \`${names.html}\`.` };
|
|
192
|
+
}
|
|
193
|
+
const ext = extname(real).toLowerCase();
|
|
194
|
+
if (!(ext in EXTENSIONS)) {
|
|
195
|
+
return { ok: false, message: `${path} is not an email source. Accepted: ${Object.keys(EXTENSIONS).join(", ")}.` };
|
|
196
|
+
}
|
|
197
|
+
const info = await stat(real);
|
|
198
|
+
if (!info.isFile()) return { ok: false, message: `${path} is not a file.` };
|
|
199
|
+
if (info.size > MAX_HTML_SIZE) {
|
|
200
|
+
return { ok: false, message: `${path} exceeds the ${Math.round(MAX_HTML_SIZE / 1024)}KB limit.` };
|
|
201
|
+
}
|
|
202
|
+
return { ok: true, html: await readFile(real, "utf8"), format: format ?? EXTENSIONS[ext], path: real };
|
|
203
|
+
}
|
|
204
|
+
function withoutSource(prompt, html, format, path) {
|
|
205
|
+
return prompt.replace(`\`\`\`${format}
|
|
206
|
+
${html}
|
|
207
|
+
\`\`\``, () => `The source is in \`${path}\`. Read and edit it there.`);
|
|
208
|
+
}
|
|
209
|
+
|
|
141
210
|
// src/hosted.ts
|
|
142
211
|
async function apiRequest(path, options) {
|
|
143
212
|
const { apiKey, apiUrl, method = "GET", body, timeoutMs = 3e4 } = options;
|
|
@@ -226,8 +295,10 @@ function worstScore(scores) {
|
|
|
226
295
|
}
|
|
227
296
|
var formatEnum = z.enum(["html", "jsx", "mjml", "maizzle"]).optional();
|
|
228
297
|
var detailEnum = z.enum(["summary", "full"]).optional().describe(
|
|
229
|
-
"'summary' (default) returns one entry per
|
|
298
|
+
"'summary' (default) returns one entry per error or warning, listing the clients it affects, and only counts info findings (safe fallbacks). 'full' returns one entry per client with fix snippets, info included."
|
|
230
299
|
);
|
|
300
|
+
var htmlParam = z.string().optional().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so. Pass this or `path`.");
|
|
301
|
+
var pathParam = z.string().optional().describe("Absolute path to the email source file (.html, .htm, .mjml, .jsx, .tsx, .vue), instead of `html`. Prefer it when the email is on disk: the source stays out of your context. The extension sets `format` unless you pass one.");
|
|
231
302
|
var targetingPolicyEnum = z.enum(["progressive", "strict", "lenient"]).optional().describe(
|
|
232
303
|
"'progressive' (default) suppresses compatibility false-positives inside client-targeted scopes and only flags deprecated or dangerous hacks. 'strict' treats targeting as non-standard and reports every hack. 'lenient' suppresses false positives and ignores non-fatal targeting warnings."
|
|
233
304
|
);
|
|
@@ -278,11 +349,14 @@ function compatibilityPayload(warnings, detail, clients) {
|
|
|
278
349
|
}))
|
|
279
350
|
};
|
|
280
351
|
}
|
|
281
|
-
const
|
|
352
|
+
const collapsed = collapseWarnings(scoped);
|
|
353
|
+
const findings = collapsed.filter((f) => f.severity !== "info");
|
|
354
|
+
const info = collapsed.length - findings.length;
|
|
282
355
|
return {
|
|
283
356
|
warningCount: scoped.length,
|
|
284
357
|
findingCount: findings.length,
|
|
285
|
-
findings
|
|
358
|
+
findings,
|
|
359
|
+
...info ? { infoFindings: info, infoNote: "Safe fallbacks, not problems. Pass detail:'full' to list them." } : {}
|
|
286
360
|
};
|
|
287
361
|
}
|
|
288
362
|
function positionsApply(format) {
|
|
@@ -299,14 +373,14 @@ function withPositions(w) {
|
|
|
299
373
|
};
|
|
300
374
|
}
|
|
301
375
|
function validateHtmlSize(html) {
|
|
302
|
-
if (html.length >
|
|
303
|
-
return mcpError(`HTML input exceeds ${Math.round(
|
|
376
|
+
if (html.length > MAX_HTML_SIZE2) {
|
|
377
|
+
return mcpError(`HTML input exceeds ${Math.round(MAX_HTML_SIZE2 / 1024)}KB limit. Reduce the email size and try again.`);
|
|
304
378
|
}
|
|
305
379
|
return null;
|
|
306
380
|
}
|
|
307
381
|
var server = new McpServer({
|
|
308
382
|
name: "emailens",
|
|
309
|
-
version: "0.
|
|
383
|
+
version: "0.13.0"
|
|
310
384
|
});
|
|
311
385
|
server.registerTool(
|
|
312
386
|
"preview_email",
|
|
@@ -314,7 +388,8 @@ server.registerTool(
|
|
|
314
388
|
title: "Preview Email",
|
|
315
389
|
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.`,
|
|
316
390
|
inputSchema: {
|
|
317
|
-
html:
|
|
391
|
+
html: htmlParam,
|
|
392
|
+
path: pathParam,
|
|
318
393
|
clients: z.array(z.string()).optional().describe("Optional client ID filter (e.g. ['gmail-web', 'outlook-windows'])"),
|
|
319
394
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
|
|
320
395
|
targetingPolicy: targetingPolicyEnum
|
|
@@ -327,7 +402,10 @@ server.registerTool(
|
|
|
327
402
|
openWorldHint: false
|
|
328
403
|
}
|
|
329
404
|
},
|
|
330
|
-
async ({ html, clients, format, targetingPolicy }) => {
|
|
405
|
+
async ({ html: inline, path, clients, format: given, targetingPolicy }) => {
|
|
406
|
+
const input = await readSource(inline, path, given);
|
|
407
|
+
if (!input.ok) return mcpError(input.message);
|
|
408
|
+
const { html, format } = input;
|
|
331
409
|
const sizeError = validateHtmlSize(html);
|
|
332
410
|
if (sizeError) return sizeError;
|
|
333
411
|
const source = await toHtml(html, format);
|
|
@@ -340,7 +418,7 @@ server.registerTool(
|
|
|
340
418
|
const filter = clients.filter((c) => validClientIds.has(c));
|
|
341
419
|
if (filter.length === 0) {
|
|
342
420
|
return mcpError(
|
|
343
|
-
JSON.stringify({ error: "No valid client IDs provided", validClientIds: Array.from(validClientIds) }
|
|
421
|
+
JSON.stringify({ error: "No valid client IDs provided", validClientIds: Array.from(validClientIds) })
|
|
344
422
|
);
|
|
345
423
|
}
|
|
346
424
|
transforms = filter.map((c) => transformForClient(source.html, c, framework));
|
|
@@ -384,7 +462,7 @@ server.registerTool(
|
|
|
384
462
|
if (!config.isHosted) {
|
|
385
463
|
result.tip = overallScore < 90 ? "Capture real screenshots to verify these issues across clients. Add EMAILENS_API_KEY to your MCP config \u2192 app.emailens.dev/dashboard/api?utm_source=mcp" : "Checked by Emailens against 21 email clients. Confirm on real Outlook, Gmail and Apple Mail \u2192 emailens.dev/real-devices?utm_source=mcp";
|
|
386
464
|
}
|
|
387
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
465
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
388
466
|
}
|
|
389
467
|
);
|
|
390
468
|
server.registerTool(
|
|
@@ -393,7 +471,8 @@ server.registerTool(
|
|
|
393
471
|
title: "Analyze Email",
|
|
394
472
|
description: "Fix error and warning findings. Info findings are not problems (the email falls back safely there): never remove responsive (@media) or dark-mode CSS to clear them. 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.).",
|
|
395
473
|
inputSchema: {
|
|
396
|
-
html:
|
|
474
|
+
html: htmlParam,
|
|
475
|
+
path: pathParam,
|
|
397
476
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
|
|
398
477
|
detail: detailEnum,
|
|
399
478
|
clients: clientsParam,
|
|
@@ -407,7 +486,10 @@ server.registerTool(
|
|
|
407
486
|
openWorldHint: false
|
|
408
487
|
}
|
|
409
488
|
},
|
|
410
|
-
async ({ html, format, detail, clients, targetingPolicy }) => {
|
|
489
|
+
async ({ html: inline, path, format: given, detail, clients, targetingPolicy }) => {
|
|
490
|
+
const input = await readSource(inline, path, given);
|
|
491
|
+
if (!input.ok) return mcpError(input.message);
|
|
492
|
+
const { html, format } = input;
|
|
411
493
|
const sizeError = validateHtmlSize(html);
|
|
412
494
|
if (sizeError) return sizeError;
|
|
413
495
|
const clientError = validateClients(clients);
|
|
@@ -429,9 +511,7 @@ server.registerTool(
|
|
|
429
511
|
overallScore,
|
|
430
512
|
scores,
|
|
431
513
|
...compatibilityPayload(warnings, detail, clients)
|
|
432
|
-
}
|
|
433
|
-
null,
|
|
434
|
-
2
|
|
514
|
+
}
|
|
435
515
|
)
|
|
436
516
|
}
|
|
437
517
|
]
|
|
@@ -442,15 +522,17 @@ server.registerTool(
|
|
|
442
522
|
"audit_email",
|
|
443
523
|
{
|
|
444
524
|
title: "Audit Email",
|
|
445
|
-
description: "Fix error and warning findings. Info findings are not problems (the email falls back safely there): never remove responsive (@media)
|
|
525
|
+
description: "Fix error and warning findings, and restore anything listed in designRemovals. Info findings are not problems (the email falls back safely there): never remove responsive (@media), dark-mode CSS or rounded corners to clear them. designRemovals compares this version with the first one audited for the same email (or with previousHtml): design the edits deleted, such as @media rules, dark mode, corners, gradients, VML fallbacks or images. Comprehensive email quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual fallbacks, dark-mode and mobile text contrast, design consistency, structural faults in Outlook-only VML (which lives inside conditional comments and is invisible to every other check), style survival: CSS a client parses correctly and then discards, such as a stylesheet past Gmail's 16 KB ceiling or every rule after a `}}`, which no support matrix can express, and client targeting: HowToTarget hacks such as Gmail u+.body or MSO conditionals, which do not score as css-hack. For HTML input, findings tied to a specific element carry loc (line, column, offset) so you can edit the exact source. Compatibility is collapsed to one finding per problem listing the clients it affects; pass detail:'full' for the per-client breakdown with fix snippets. Use skip to omit checks, targetingPolicy to change how targeted scopes affect scores, and clients to narrow which clients are reported.",
|
|
446
526
|
inputSchema: {
|
|
447
|
-
html:
|
|
527
|
+
html: htmlParam,
|
|
528
|
+
path: pathParam,
|
|
448
529
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
|
|
449
530
|
skip: z.array(skipEnum).optional().describe("Checks to skip (e.g. ['spam', 'images'])"),
|
|
450
531
|
detail: detailEnum,
|
|
451
532
|
clients: clientsParam,
|
|
452
533
|
targetingPolicy: targetingPolicyEnum,
|
|
453
|
-
emailType: z.enum(["marketing", "transactional"]).optional().describe("'transactional' (receipts, password resets, codes, alerts: no unsubscribe link required) or 'marketing'. Omitted: guessed from the content, which misses many transactional emails.")
|
|
534
|
+
emailType: z.enum(["marketing", "transactional"]).optional().describe("'transactional' (receipts, password resets, codes, alerts: no unsubscribe link required) or 'marketing'. Omitted: guessed from the content, which misses many transactional emails."),
|
|
535
|
+
previousHtml: z.string().optional().describe("The design to hold this version to, usually your first draft. Omitted: the first version of this email audited in this session (matched by <title> and text).")
|
|
454
536
|
},
|
|
455
537
|
annotations: {
|
|
456
538
|
title: "Audit Email",
|
|
@@ -460,7 +542,10 @@ server.registerTool(
|
|
|
460
542
|
openWorldHint: false
|
|
461
543
|
}
|
|
462
544
|
},
|
|
463
|
-
async ({ html, format, skip, detail, clients, targetingPolicy, emailType }) => {
|
|
545
|
+
async ({ html: inline, path, format: given, skip, detail, clients, targetingPolicy, emailType, previousHtml }) => {
|
|
546
|
+
const input = await readSource(inline, path, given);
|
|
547
|
+
if (!input.ok) return mcpError(input.message);
|
|
548
|
+
const { html, format } = input;
|
|
464
549
|
const sizeError = validateHtmlSize(html);
|
|
465
550
|
if (sizeError) return sizeError;
|
|
466
551
|
const clientError = validateClients(clients);
|
|
@@ -474,6 +559,8 @@ server.registerTool(
|
|
|
474
559
|
});
|
|
475
560
|
const report = session.audit({ skip, ...emailType ? { spam: { emailType } } : {} });
|
|
476
561
|
const overallCompatibility = worstScore(report.compatibility.scores);
|
|
562
|
+
const baseline = previousHtml ? await toHtml(previousHtml, format) : { ok: true, html: firstDraft(source.html) };
|
|
563
|
+
const designRemovals = baseline.ok ? checkDesignRetention(baseline.html, source.html) : [];
|
|
477
564
|
return {
|
|
478
565
|
content: [
|
|
479
566
|
{
|
|
@@ -481,6 +568,7 @@ server.registerTool(
|
|
|
481
568
|
text: JSON.stringify(
|
|
482
569
|
{
|
|
483
570
|
overallCompatibility,
|
|
571
|
+
designRemovals,
|
|
484
572
|
compatibility: {
|
|
485
573
|
scores: report.compatibility.scores,
|
|
486
574
|
...compatibilityPayload(report.compatibility.warnings, detail, clients)
|
|
@@ -500,9 +588,7 @@ server.registerTool(
|
|
|
500
588
|
design: report.design,
|
|
501
589
|
styleSurvival: report.styleSurvival,
|
|
502
590
|
targeting: report.targeting
|
|
503
|
-
}
|
|
504
|
-
null,
|
|
505
|
-
2
|
|
591
|
+
}
|
|
506
592
|
)
|
|
507
593
|
}
|
|
508
594
|
]
|
|
@@ -515,7 +601,8 @@ server.registerTool(
|
|
|
515
601
|
title: "Fix Email",
|
|
516
602
|
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.",
|
|
517
603
|
inputSchema: {
|
|
518
|
-
html:
|
|
604
|
+
html: htmlParam,
|
|
605
|
+
path: pathParam,
|
|
519
606
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
|
|
520
607
|
scope: z.enum(["all", "current"]).optional().describe("'all' (default) or 'current' (requires selectedClientId)"),
|
|
521
608
|
selectedClientId: z.string().optional().describe("Client ID to scope fixes to (e.g. 'outlook-windows')")
|
|
@@ -528,7 +615,10 @@ server.registerTool(
|
|
|
528
615
|
openWorldHint: false
|
|
529
616
|
}
|
|
530
617
|
},
|
|
531
|
-
async ({ html, format, scope, selectedClientId }) => {
|
|
618
|
+
async ({ html: inline, path, format: given, scope, selectedClientId }) => {
|
|
619
|
+
const input = await readSource(inline, path, given);
|
|
620
|
+
if (!input.ok) return mcpError(input.message);
|
|
621
|
+
const { html, format } = input;
|
|
532
622
|
const sizeError = validateHtmlSize(html);
|
|
533
623
|
if (sizeError) return sizeError;
|
|
534
624
|
const source = await toHtml(html, format);
|
|
@@ -544,7 +634,7 @@ server.registerTool(
|
|
|
544
634
|
content: [
|
|
545
635
|
{
|
|
546
636
|
type: "text",
|
|
547
|
-
text: JSON.stringify({ message: "No compatibility issues found \u2014 nothing to fix.", overallScore: 100 }
|
|
637
|
+
text: JSON.stringify({ message: "No compatibility issues found \u2014 nothing to fix.", overallScore: 100 })
|
|
548
638
|
}
|
|
549
639
|
]
|
|
550
640
|
};
|
|
@@ -581,12 +671,10 @@ server.registerTool(
|
|
|
581
671
|
truncated: estimate.truncated
|
|
582
672
|
},
|
|
583
673
|
note: "Apply the fixes in the prompt below. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes."
|
|
584
|
-
}
|
|
585
|
-
null,
|
|
586
|
-
2
|
|
674
|
+
}
|
|
587
675
|
)
|
|
588
676
|
},
|
|
589
|
-
{ type: "text", text: prompt }
|
|
677
|
+
{ type: "text", text: input.path ? withoutSource(prompt, html, inputFormat, input.path) : prompt }
|
|
590
678
|
]
|
|
591
679
|
};
|
|
592
680
|
}
|
|
@@ -617,9 +705,7 @@ server.registerTool(
|
|
|
617
705
|
engine: c.engine,
|
|
618
706
|
darkModeSupport: c.darkModeSupport,
|
|
619
707
|
...c.deprecated ? { deprecated: c.deprecated } : {}
|
|
620
|
-
}))
|
|
621
|
-
null,
|
|
622
|
-
2
|
|
708
|
+
}))
|
|
623
709
|
)
|
|
624
710
|
}
|
|
625
711
|
]
|
|
@@ -632,8 +718,10 @@ server.registerTool(
|
|
|
632
718
|
title: "Diff Emails",
|
|
633
719
|
description: "Compare two email HTML versions, shows score changes, fixed issues, and newly introduced issues per client. Use after making fixes to verify improvements.",
|
|
634
720
|
inputSchema: {
|
|
635
|
-
before: z.string().describe("Original email source"),
|
|
636
|
-
after: z.string().describe("Modified email source, in the same format"),
|
|
721
|
+
before: z.string().optional().describe("Original email source. Pass this or `beforePath`."),
|
|
722
|
+
after: z.string().optional().describe("Modified email source, in the same format. Pass this or `afterPath`."),
|
|
723
|
+
beforePath: z.string().optional().describe("Absolute path to the original source file, instead of `before`"),
|
|
724
|
+
afterPath: z.string().optional().describe("Absolute path to the modified source file, instead of `after`"),
|
|
637
725
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.")
|
|
638
726
|
},
|
|
639
727
|
annotations: {
|
|
@@ -644,7 +732,14 @@ server.registerTool(
|
|
|
644
732
|
openWorldHint: false
|
|
645
733
|
}
|
|
646
734
|
},
|
|
647
|
-
async ({ before, after, format }) => {
|
|
735
|
+
async ({ before: beforeInline, after: afterInline, beforePath, afterPath, format: given }) => {
|
|
736
|
+
const beforeInput = await readSource(beforeInline, beforePath, given, { html: "before", path: "beforePath" });
|
|
737
|
+
if (!beforeInput.ok) return mcpError(beforeInput.message);
|
|
738
|
+
const afterInput = await readSource(afterInline, afterPath, given, { html: "after", path: "afterPath" });
|
|
739
|
+
if (!afterInput.ok) return mcpError(afterInput.message);
|
|
740
|
+
const before = beforeInput.html;
|
|
741
|
+
const after = afterInput.html;
|
|
742
|
+
const format = afterInput.format ?? beforeInput.format;
|
|
648
743
|
const beforeSizeError = validateHtmlSize(before);
|
|
649
744
|
if (beforeSizeError) return beforeSizeError;
|
|
650
745
|
const afterSizeError = validateHtmlSize(after);
|
|
@@ -696,9 +791,7 @@ server.registerTool(
|
|
|
696
791
|
clientsUnchanged,
|
|
697
792
|
avgScoreDelta: results.length > 0 ? Math.round(totalDelta / results.length * 10) / 10 : 0
|
|
698
793
|
}
|
|
699
|
-
}
|
|
700
|
-
null,
|
|
701
|
-
2
|
|
794
|
+
}
|
|
702
795
|
)
|
|
703
796
|
}
|
|
704
797
|
]
|
|
@@ -729,7 +822,7 @@ server.registerTool(
|
|
|
729
822
|
}
|
|
730
823
|
const report = await _checkDeliverability(domain);
|
|
731
824
|
return {
|
|
732
|
-
content: [{ type: "text", text: JSON.stringify(report
|
|
825
|
+
content: [{ type: "text", text: JSON.stringify(report) }]
|
|
733
826
|
};
|
|
734
827
|
} catch (err) {
|
|
735
828
|
const msg = err.message ?? String(err);
|
|
@@ -748,7 +841,8 @@ server.registerTool(
|
|
|
748
841
|
title: "Capture Screenshots",
|
|
749
842
|
description: "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?utm_source=mcp.",
|
|
750
843
|
inputSchema: {
|
|
751
|
-
html:
|
|
844
|
+
html: htmlParam,
|
|
845
|
+
path: pathParam,
|
|
752
846
|
format: formatEnum.describe("Input format"),
|
|
753
847
|
clients: z.array(z.string()).optional().describe("Filter to specific client IDs"),
|
|
754
848
|
modes: z.array(z.enum(["light", "dark"])).optional().describe("Screenshot modes (default: ['light'])"),
|
|
@@ -762,14 +856,17 @@ server.registerTool(
|
|
|
762
856
|
openWorldHint: true
|
|
763
857
|
}
|
|
764
858
|
},
|
|
765
|
-
async ({ html, format, clients, modes, title }) => {
|
|
859
|
+
async ({ html: inline, path, format: given, clients, modes, title: title2 }) => {
|
|
860
|
+
const input = await readSource(inline, path, given);
|
|
861
|
+
if (!input.ok) return mcpError(input.message);
|
|
862
|
+
const { html, format } = input;
|
|
766
863
|
if (!config.isHosted) {
|
|
767
864
|
return noApiKeyError("capture_screenshots");
|
|
768
865
|
}
|
|
769
866
|
const opts = { apiKey: config.apiKey, apiUrl: config.apiUrl };
|
|
770
867
|
const previewResult = await apiRequest("/api/preview", {
|
|
771
868
|
...opts,
|
|
772
|
-
body: { html, format, clients, title, source: "mcp" },
|
|
869
|
+
body: { html, format, clients, title: title2, source: "mcp" },
|
|
773
870
|
timeoutMs: 3e4
|
|
774
871
|
});
|
|
775
872
|
if (previewResult.isError) {
|
|
@@ -803,9 +900,7 @@ server.registerTool(
|
|
|
803
900
|
captured: [],
|
|
804
901
|
failed: [],
|
|
805
902
|
note: `Preview created but screenshot capture failed: ${screenshotResult.error}. View the full preview at the URL above.`
|
|
806
|
-
}
|
|
807
|
-
null,
|
|
808
|
-
2
|
|
903
|
+
}
|
|
809
904
|
)
|
|
810
905
|
}
|
|
811
906
|
]
|
|
@@ -824,7 +919,7 @@ server.registerTool(
|
|
|
824
919
|
if (ss.failed?.length > 0) {
|
|
825
920
|
result.note = `${ss.failed.length} of ${(ss.captured?.length ?? 0) + ss.failed.length} screenshots failed: ${ss.failed.join(", ")}`;
|
|
826
921
|
}
|
|
827
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
922
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
828
923
|
}
|
|
829
924
|
);
|
|
830
925
|
server.registerTool(
|
|
@@ -833,7 +928,8 @@ server.registerTool(
|
|
|
833
928
|
title: "Share Preview",
|
|
834
929
|
description: "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).",
|
|
835
930
|
inputSchema: {
|
|
836
|
-
html:
|
|
931
|
+
html: htmlParam,
|
|
932
|
+
path: pathParam,
|
|
837
933
|
title: z.string().optional().describe("Display title for the share page"),
|
|
838
934
|
format: formatEnum.describe("Input format")
|
|
839
935
|
},
|
|
@@ -845,14 +941,17 @@ server.registerTool(
|
|
|
845
941
|
openWorldHint: true
|
|
846
942
|
}
|
|
847
943
|
},
|
|
848
|
-
async ({ html, title, format }) => {
|
|
944
|
+
async ({ html: inline, path, title: title2, format: given }) => {
|
|
945
|
+
const input = await readSource(inline, path, given);
|
|
946
|
+
if (!input.ok) return mcpError(input.message);
|
|
947
|
+
const { html, format } = input;
|
|
849
948
|
if (!config.isHosted) {
|
|
850
949
|
return noApiKeyError("share_preview", "Shareable links require the Dev plan ($9/mo).");
|
|
851
950
|
}
|
|
852
951
|
const result = await apiRequest("/api/share", {
|
|
853
952
|
apiKey: config.apiKey,
|
|
854
953
|
apiUrl: config.apiUrl,
|
|
855
|
-
body: { html, title, format }
|
|
954
|
+
body: { html, title: title2, format }
|
|
856
955
|
});
|
|
857
956
|
if (result.isError) {
|
|
858
957
|
return mcpError(result.error);
|
|
@@ -865,9 +964,7 @@ server.registerTool(
|
|
|
865
964
|
{
|
|
866
965
|
shareUrl: `${config.apiUrl}/share/${result.data.id}`,
|
|
867
966
|
expiresAt: result.data.expiresAt
|
|
868
|
-
}
|
|
869
|
-
null,
|
|
870
|
-
2
|
|
967
|
+
}
|
|
871
968
|
)
|
|
872
969
|
}
|
|
873
970
|
]
|
|
@@ -905,7 +1002,7 @@ server.registerTool(
|
|
|
905
1002
|
content: [
|
|
906
1003
|
{
|
|
907
1004
|
type: "text",
|
|
908
|
-
text: JSON.stringify(result.data
|
|
1005
|
+
text: JSON.stringify(result.data)
|
|
909
1006
|
}
|
|
910
1007
|
]
|
|
911
1008
|
};
|
|
@@ -951,7 +1048,7 @@ server.registerTool(
|
|
|
951
1048
|
content: [
|
|
952
1049
|
{
|
|
953
1050
|
type: "text",
|
|
954
|
-
text: JSON.stringify(result.data?.message ?? result.data
|
|
1051
|
+
text: JSON.stringify(result.data?.message ?? result.data)
|
|
955
1052
|
}
|
|
956
1053
|
]
|
|
957
1054
|
};
|
|
@@ -989,7 +1086,7 @@ server.registerTool(
|
|
|
989
1086
|
content: [
|
|
990
1087
|
{
|
|
991
1088
|
type: "text",
|
|
992
|
-
text: JSON.stringify(result.data
|
|
1089
|
+
text: JSON.stringify(result.data)
|
|
993
1090
|
}
|
|
994
1091
|
]
|
|
995
1092
|
};
|
|
@@ -1026,7 +1123,7 @@ server.registerTool(
|
|
|
1026
1123
|
content: [
|
|
1027
1124
|
{
|
|
1028
1125
|
type: "text",
|
|
1029
|
-
text: JSON.stringify(result.data
|
|
1126
|
+
text: JSON.stringify(result.data)
|
|
1030
1127
|
}
|
|
1031
1128
|
]
|
|
1032
1129
|
};
|
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\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { collapseWarnings, forClients, knownClientIds } from \"./findings.js\";\nimport { resolveSourceFormat, toHtml } from \"./compile.js\";\nimport {\n createSession,\n analyzeEmail,\n generateCompatibilityScore,\n simulateDarkMode,\n transformForAllClients,\n transformForClient,\n generateFixPrompt,\n estimateAiFixTokens,\n diffResults,\n toPlainText,\n EMAIL_CLIENTS,\n MAX_HTML_SIZE,\n type Framework,\n type CSSWarning,\n type SourceLocation,\n} from \"@emailens/engine\";\n// Lazy-imported: @emailens/engine/server may not exist in all engine versions\nlet _checkDeliverability: ((domain: string) => Promise<unknown>) | null = null;\nimport { config } from \"./config.js\";\nimport { apiRequest, mcpError, noApiKeyError } from \"./hosted.js\";\n\nfunction toFramework(format?: string): Framework | undefined {\n if (format === \"jsx\" || format === \"mjml\" || format === \"maizzle\") return format;\n return undefined;\n}\n\n/** The headline is the worst client. A mean of 21 clients hides the one that fails. */\nfunction worstScore(scores: Record<string, { score: number }>): number {\n const values = Object.values(scores);\n if (values.length === 0) return 0;\n return Math.min(...values.map((s) => s.score));\n}\n\n/**\n * The language the source is written in.\n *\n * Anything but `html` is compiled before analysis and also selects the syntax\n * the fix snippets are written in.\n */\nconst formatEnum = z.enum([\"html\", \"jsx\", \"mjml\", \"maizzle\"]).optional();\n\n/**\n * How much compatibility detail to return.\n *\n * `summary` collapses the engine's per-client, per-selector warnings into one\n * entry per problem listing the clients it affects: the same information at a\n * fraction of the tokens. `full` is the engine's own shape, per client, with\n * fix snippets, for a caller that wants everything.\n */\nconst detailEnum = z\n .enum([\"summary\", \"full\"])\n .optional()\n .describe(\n \"'summary' (default) returns one entry per problem, listing the clients it affects (roughly 10x smaller). 'full' returns one entry per client with fix snippets.\",\n );\n\nconst targetingPolicyEnum = z\n .enum([\"progressive\", \"strict\", \"lenient\"])\n .optional()\n .describe(\n \"'progressive' (default) suppresses compatibility false-positives inside client-targeted scopes and only flags deprecated or dangerous hacks. 'strict' treats targeting as non-standard and reports every hack. 'lenient' suppresses false positives and ignores non-fatal targeting warnings.\",\n );\n\nconst skipEnum = z.enum([\n \"spam\",\n \"links\",\n \"accessibility\",\n \"images\",\n \"compatibility\",\n \"inboxPreview\",\n \"size\",\n \"templateVariables\",\n \"overflow\",\n \"visual\",\n \"darkContrast\",\n \"mobileContrast\",\n \"design\",\n \"vml\",\n \"styleSurvival\",\n \"targeting\",\n]);\n\nconst clientsParam = z\n .array(z.string())\n .optional()\n .describe(\n \"Only report these client IDs (e.g. ['gmail-web','outlook-windows']). The fastest way to cut the response when you care about specific clients. Use list_clients for the IDs.\",\n );\n\n/**\n * Reject a client id the engine does not know.\n *\n * Filtering to a typo would return an empty list, which reads as \"this email\n * is fine for that client\"; the most misleading answer the tool can give.\n */\nfunction validateClients(clients: string[] | undefined) {\n if (!clients?.length) return undefined;\n const known = new Set(knownClientIds());\n const unknown = clients.filter((c) => !known.has(c));\n if (!unknown.length) return undefined;\n return mcpError(\n `Unknown client ${unknown.length > 1 ? \"IDs\" : \"ID\"}: ${unknown.join(\", \")}. Call list_clients for the valid IDs.`,\n );\n}\n\n/** The compatibility payload, at the requested level of detail. */\nfunction compatibilityPayload(\n warnings: CSSWarning[],\n detail: \"summary\" | \"full\" | undefined,\n clients: string[] | undefined,\n) {\n const scoped = forClients(warnings, clients);\n if (detail === \"full\") {\n return {\n warningCount: scoped.length,\n warnings: scoped.map((w) => ({\n client: w.client,\n property: w.property,\n severity: w.severity,\n message: w.message,\n suggestion: w.suggestion,\n fix: w.fix,\n fixType: w.fixType,\n ...withPositions(w),\n })),\n };\n }\n const findings = collapseWarnings(scoped);\n return {\n warningCount: scoped.length,\n findingCount: findings.length,\n findings,\n };\n}\n\n/**\n * Do source positions refer to the code the caller handed us?\n *\n * Only for plain HTML. JSX, MJML and Maizzle are compiled before analysis, so a\n * position would point into generated output: worse than none, because an\n * agent would edit the wrong line with confidence.\n */\nfunction positionsApply(format?: string): boolean {\n return (format ?? \"html\") === \"html\";\n}\n\n/**\n * The position fields a finding carries, when it has them.\n *\n * Deliberately compact: this response is read by a model paying for every\n * token, and a newsletter can produce a thousand occurrences. The first one\n * gets a full position, enough to edit it, and the rest are reduced to line\n * numbers, which is all an assistant needs to go find them. Carrying every\n * occurrence in full nearly doubled the payload on a real fixture.\n */\nfunction withPositions<T extends {\n loc?: SourceLocation;\n locs?: SourceLocation[];\n locsTruncated?: boolean;\n}>(w: T) {\n if (!w.loc) return {};\n const { line, column, offset, length } = w.loc;\n const others = (w.locs ?? []).slice(1).map((l) => l.line);\n return {\n loc: { line, column, offset, length },\n ...(others.length ? { alsoAtLines: others } : {}),\n ...(w.locsTruncated ? { locsTruncated: true } : {}),\n };\n}\n\nfunction validateHtmlSize(html: string) {\n if (html.length > MAX_HTML_SIZE) {\n return mcpError(`HTML input exceeds ${Math.round(MAX_HTML_SIZE / 1024)}KB limit. Reduce the email size and try again.`);\n }\n return null;\n}\n\nconst server = new McpServer({\n name: \"emailens\",\n version: \"0.12.0\",\n});\n\n// ── Local Tool: preview_email ──────────────────────────────────────\n\nserver.registerTool(\n \"preview_email\",\n {\n title: \"Preview Email\",\n description:\n `Full email compatibility preview, transforms HTML for ${EMAIL_CLIENTS.length} email clients (Gmail, Outlook, Apple Mail, Yahoo, Samsung, Thunderbird, HEY, Proton Mail, AOL, Fastmail, Superhuman), analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.`,\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n clients: z\n .array(z.string())\n .optional()\n .describe(\"Optional client ID filter (e.g. ['gmail-web', 'outlook-windows'])\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n targetingPolicy: targetingPolicyEnum,\n },\n annotations: {\n title: \"Preview Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html, clients, format, targetingPolicy }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const validClientIds = new Set(EMAIL_CLIENTS.map((c) => c.id));\n const framework = toFramework(resolveSourceFormat(html, format));\n const session = createSession(source.html, { framework, targetingPolicy });\n\n let transforms;\n if (clients) {\n const filter = clients.filter((c) => validClientIds.has(c));\n if (filter.length === 0) {\n return mcpError(\n JSON.stringify({ error: \"No valid client IDs provided\", validClientIds: Array.from(validClientIds) }, null, 2),\n );\n }\n transforms = filter.map((c) => transformForClient(source.html, c, framework));\n } else {\n transforms = transformForAllClients(source.html, framework);\n }\n\n const warnings = session.analyze();\n const scores = generateCompatibilityScore(warnings);\n const inboxPreview = session.extractInboxPreview();\n const sizeReport = session.checkSize();\n\n const darkMode: Record<string, { html: string; warnings: CSSWarning[] }> = {};\n for (const t of transforms) {\n darkMode[t.clientId] = simulateDarkMode(source.html, t.clientId);\n }\n\n const overallScore = worstScore(scores);\n\n const plainText = toPlainText(source.html);\n\n const result: Record<string, unknown> = {\n overallScore,\n compatibilityScores: scores,\n cssWarnings: warnings.map((w) => ({\n client: w.client,\n property: w.property,\n severity: w.severity,\n message: w.message,\n suggestion: w.suggestion,\n fix: w.fix,\n fixType: w.fixType,\n })),\n inboxPreview,\n sizeReport,\n ...(plainText ? { plainText } : {}),\n clientCount: transforms.length,\n darkModeWarnings: Object.entries(darkMode).reduce(\n (acc, [clientId, dm]) => {\n if (dm.warnings.length > 0) acc[clientId] = dm.warnings.length;\n return acc;\n },\n {} as Record<string, number>,\n ),\n };\n\n // Contextual tip when no API key: the fix route on issues, and on a clean\n // result the name of who checked it, so success is attributable too.\n if (!config.isHosted) {\n result.tip = overallScore < 90\n ? \"Capture real screenshots to verify these issues across clients. Add EMAILENS_API_KEY to your MCP config \\u2192 app.emailens.dev/dashboard/api?utm_source=mcp\"\n : \"Checked by Emailens against 21 email clients. Confirm on real Outlook, Gmail and Apple Mail \\u2192 emailens.dev/real-devices?utm_source=mcp\";\n }\n\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] };\n },\n);\n\n// ── Local Tool: analyze_email (CSS-only, reverted from audit) ──────\n\nserver.registerTool(\n \"analyze_email\",\n {\n title: \"Analyze Email\",\n description:\n \"Fix error and warning findings. Info findings are not problems (the email falls back safely there): never remove responsive (@media) or dark-mode CSS to clear them. Quick CSS compatibility analysis, returns per-client scores and one finding per problem, listing the clients it affects. For HTML input each finding carries loc (line, column, offset) and alsoAtLines, so you can edit the exact source. Narrow with clients, or pass detail:'full' for the per-client breakdown with fix snippets. Use audit_email for a full quality report (spam, links, a11y, images, etc.).\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n detail: detailEnum,\n clients: clientsParam,\n targetingPolicy: targetingPolicyEnum,\n },\n annotations: {\n title: \"Analyze Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html, format, detail, clients, targetingPolicy }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n const clientError = validateClients(clients);\n if (clientError) return clientError;\n\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const warnings = analyzeEmail(source.html, toFramework(resolveSourceFormat(html, format)), {\n positions: positionsApply(resolveSourceFormat(html, format)),\n targetingPolicy,\n });\n // Scores stay whole-email: filtering to two clients should narrow what is\n // reported, not silently change what the email scores.\n const scores = generateCompatibilityScore(warnings);\n\n const overallScore = worstScore(scores);\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n overallScore,\n scores,\n ...compatibilityPayload(warnings, detail, clients),\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: audit_email ────────────────────────────────────────\n\nserver.registerTool(\n \"audit_email\",\n {\n title: \"Audit Email\",\n description:\n \"Fix error and warning findings. Info findings are not problems (the email falls back safely there): never remove responsive (@media) or dark-mode CSS to clear them. Comprehensive email quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual fallbacks, dark-mode and mobile text contrast, design consistency, structural faults in Outlook-only VML (which lives inside conditional comments and is invisible to every other check), style survival: CSS a client parses correctly and then discards, such as a stylesheet past Gmail's 16 KB ceiling or every rule after a `}}`, which no support matrix can express, and client targeting: HowToTarget hacks such as Gmail u+.body or MSO conditionals, which do not score as css-hack. For HTML input, findings tied to a specific element carry loc (line, column, offset) so you can edit the exact source. Compatibility is collapsed to one finding per problem listing the clients it affects; pass detail:'full' for the per-client breakdown with fix snippets. Use skip to omit checks, targetingPolicy to change how targeted scopes affect scores, and clients to narrow which clients are reported.\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n skip: z\n .array(skipEnum)\n .optional()\n .describe(\"Checks to skip (e.g. ['spam', 'images'])\"),\n detail: detailEnum,\n clients: clientsParam,\n targetingPolicy: targetingPolicyEnum,\n emailType: z\n .enum([\"marketing\", \"transactional\"])\n .optional()\n .describe(\"'transactional' (receipts, password resets, codes, alerts: no unsubscribe link required) or 'marketing'. Omitted: guessed from the content, which misses many transactional emails.\"),\n },\n annotations: {\n title: \"Audit Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html, format, skip, detail, clients, targetingPolicy, emailType }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n const clientError = validateClients(clients);\n if (clientError) return clientError;\n\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const session = createSession(source.html, {\n framework: toFramework(resolveSourceFormat(html, format)),\n positions: positionsApply(resolveSourceFormat(html, format)),\n targetingPolicy,\n });\n const report = session.audit({ skip, ...(emailType ? { spam: { emailType } } : {}) });\n\n const overallCompatibility = worstScore(report.compatibility.scores);\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n overallCompatibility,\n compatibility: {\n scores: report.compatibility.scores,\n ...compatibilityPayload(report.compatibility.warnings, detail, clients),\n },\n spam: report.spam,\n links: report.links,\n accessibility: report.accessibility,\n images: report.images,\n inboxPreview: report.inboxPreview,\n size: report.size,\n templateVariables: report.templateVariables,\n overflow: report.overflow,\n visual: report.visual,\n vml: report.vml,\n darkContrast: report.darkContrast,\n mobileContrast: report.mobileContrast,\n design: report.design,\n styleSurvival: report.styleSurvival,\n targeting: report.targeting,\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: fix_email ──────────────────────────────────────────\n\nserver.registerTool(\n \"fix_email\",\n {\n title: \"Fix Email\",\n description:\n \"Generate a structured fix prompt for email compatibility issues. Returns markdown with the original code, detected issues (CSS or structural), fix snippets, and format-specific instructions. Use after preview_email or analyze_email.\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so: the source to fix\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n scope: z.enum([\"all\", \"current\"]).optional().describe(\"'all' (default) or 'current' (requires selectedClientId)\"),\n selectedClientId: z.string().optional().describe(\"Client ID to scope fixes to (e.g. 'outlook-windows')\"),\n },\n annotations: {\n title: \"Fix Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html, format, scope, selectedClientId }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n\n // The findings come from the compiled output, because that is what a\n // client renders. The prompt keeps the caller's own source, because that is\n // the file they will edit; a fix written against generated HTML is not a\n // fix they can apply.\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const resolved = resolveSourceFormat(html, format) as\n | \"html\"\n | \"jsx\"\n | \"mjml\"\n | \"maizzle\";\n const framework = toFramework(resolved);\n const fixScope = scope === \"current\" ? \"current\" : \"all\";\n const inputFormat = resolved;\n\n const warnings = analyzeEmail(source.html, framework);\n const scores = generateCompatibilityScore(warnings);\n\n if (warnings.length === 0) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify({ message: \"No compatibility issues found \\u2014 nothing to fix.\", overallScore: 100 }, null, 2),\n },\n ],\n };\n }\n\n const estimate = await estimateAiFixTokens({\n originalHtml: html,\n warnings,\n scores,\n scope: fixScope,\n selectedClientId,\n format: inputFormat,\n });\n\n const prompt = generateFixPrompt({\n originalHtml: html,\n warnings,\n scores,\n scope: fixScope,\n selectedClientId,\n format: inputFormat,\n });\n\n const structural = warnings.filter((w) => w.fixType === \"structural\");\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n totalWarnings: warnings.length,\n structuralWarnings: structural.length,\n cssWarnings: warnings.length - structural.length,\n tokenEstimate: {\n inputTokens: estimate.inputTokens,\n estimatedOutputTokens: estimate.estimatedOutputTokens,\n truncated: estimate.truncated,\n },\n note: \"Apply the fixes in the prompt below. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes.\",\n },\n null,\n 2,\n ),\n },\n { type: \"text\" as const, text: prompt },\n ],\n };\n },\n);\n\n// ── Local Tool: list_clients ───────────────────────────────────────\n\nserver.registerTool(\n \"list_clients\",\n {\n title: \"List Email Clients\",\n description: \"List every supported email client with IDs, names, engines, and dark mode support.\",\n annotations: {\n title: \"List Email Clients\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async () => {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n EMAIL_CLIENTS.map((c) => ({\n id: c.id,\n name: c.name,\n category: c.category,\n engine: c.engine,\n darkModeSupport: c.darkModeSupport,\n ...(c.deprecated ? { deprecated: c.deprecated } : {}),\n })),\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: diff_emails ────────────────────────────────────────\n\nserver.registerTool(\n \"diff_emails\",\n {\n title: \"Diff Emails\",\n description:\n \"Compare two email HTML versions, shows score changes, fixed issues, and newly introduced issues per client. Use after making fixes to verify improvements.\",\n inputSchema: {\n before: z.string().describe(\"Original email source\"),\n after: z.string().describe(\"Modified email source, in the same format\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n },\n annotations: {\n title: \"Diff Emails\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ before, after, format }) => {\n const beforeSizeError = validateHtmlSize(before);\n if (beforeSizeError) return beforeSizeError;\n const afterSizeError = validateHtmlSize(after);\n if (afterSizeError) return afterSizeError;\n\n const beforeSource = await toHtml(before, format);\n if (!beforeSource.ok) return mcpError(beforeSource.message);\n const afterSource = await toHtml(after, format);\n if (!afterSource.ok) return mcpError(afterSource.message);\n\n const framework = toFramework(\n resolveSourceFormat(before, format) === \"maizzle\" ||\n resolveSourceFormat(after, format) === \"maizzle\"\n ? (format ?? \"maizzle\")\n : (format ?? \"html\"),\n );\n\n const beforeWarnings = analyzeEmail(beforeSource.html, framework);\n const beforeScores = generateCompatibilityScore(beforeWarnings);\n\n const afterWarnings = analyzeEmail(afterSource.html, framework);\n const afterScores = generateCompatibilityScore(afterWarnings);\n\n const results = diffResults(\n { scores: beforeScores, warnings: beforeWarnings },\n { scores: afterScores, warnings: afterWarnings },\n );\n\n let clientsImproved = 0;\n let clientsRegressed = 0;\n let clientsUnchanged = 0;\n let totalDelta = 0;\n\n const mappedResults = results.map((r) => {\n if (r.scoreDelta > 0) clientsImproved++;\n else if (r.scoreDelta < 0) clientsRegressed++;\n else clientsUnchanged++;\n totalDelta += r.scoreDelta;\n\n return {\n clientId: r.clientId,\n scoreBefore: r.scoreBefore,\n scoreAfter: r.scoreAfter,\n scoreDelta: r.scoreDelta,\n fixed: r.fixed.map((w) => ({ property: w.property, client: w.client, message: w.message })),\n introduced: r.introduced.map((w) => ({ property: w.property, client: w.client, message: w.message })),\n unchanged: r.unchanged.length,\n };\n });\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n results: mappedResults,\n summary: {\n clientsImproved,\n clientsRegressed,\n clientsUnchanged,\n avgScoreDelta: results.length > 0 ? Math.round((totalDelta / results.length) * 10) / 10 : 0,\n },\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: check_deliverability ───────────────────────────────\n\nserver.registerTool(\n \"check_deliverability\",\n {\n title: \"Check Deliverability\",\n description:\n \"Check email deliverability for a domain: SPF, DKIM, DMARC, MX, and BIMI records. Returns a score (0-100) and actionable issues. Uses DNS lookups (no API key needed).\",\n inputSchema: {\n domain: z\n .string()\n .min(3)\n .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"Invalid domain format\")\n .describe(\"Domain to check (e.g. 'company.com')\"),\n },\n annotations: {\n title: \"Check Deliverability\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ domain }) => {\n try {\n if (!_checkDeliverability) {\n const mod = await import(\"@emailens/engine/server\");\n _checkDeliverability = mod.checkDeliverability;\n }\n const report = await _checkDeliverability!(domain);\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(report, null, 2) }],\n };\n } catch (err) {\n const msg = (err as Error).message ?? String(err);\n if (msg.includes(\"Cannot find\") || msg.includes(\"cannot find\") || msg.includes(\"MODULE_NOT_FOUND\")) {\n return mcpError(\n \"check_deliverability requires @emailens/engine >=0.9.1 with the /server subpath. Update with: npm install @emailens/engine@latest\",\n );\n }\n return mcpError(`Deliverability check failed for \"${domain}\": ${msg}`);\n }\n },\n);\n\n// ── Hosted Tool: capture_screenshots ───────────────────────────────\n\nserver.registerTool(\n \"capture_screenshots\",\n {\n title: \"Capture Screenshots\",\n description:\n \"Capture real email screenshots across 21 clients (Gmail, Outlook, Apple Mail, etc.) with light and dark mode variants. Screenshots are rendered in real browsers and hosted on CDN. Requires EMAILENS_API_KEY env var \\u2014 free plan at emailens.dev?utm_source=mcp.\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n format: formatEnum.describe(\"Input format\"),\n clients: z.array(z.string()).optional().describe(\"Filter to specific client IDs\"),\n modes: z\n .array(z.enum([\"light\", \"dark\"]))\n .optional()\n .describe(\"Screenshot modes (default: ['light'])\"),\n title: z.string().optional().describe(\"Name for the hosted preview\"),\n },\n annotations: {\n title: \"Capture Screenshots\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ html, format, clients, modes, title }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"capture_screenshots\");\n }\n\n const opts = { apiKey: config.apiKey!, apiUrl: config.apiUrl };\n\n // Step 1: Create hosted preview\n const previewResult = await apiRequest<{\n id: string;\n overallScore: number;\n }>(\"/api/preview\", {\n ...opts,\n body: { html, format, clients, title, source: \"mcp\" },\n timeoutMs: 30_000,\n });\n\n if (previewResult.isError) {\n return mcpError(previewResult.error!);\n }\n\n const previewId = previewResult.data!.id;\n\n // Step 2: Capture screenshots (with one retry on timeout)\n let screenshotResult = await apiRequest<{\n screenshots: Record<string, string>;\n screenshotStatus: string;\n captured: string[];\n failed: string[];\n }>(`/api/preview/${previewId}/screenshots`, {\n ...opts,\n body: { clients, modes },\n timeoutMs: 120_000,\n });\n\n // Retry once on timeout\n if (screenshotResult.isError && screenshotResult.error?.includes(\"timed out\")) {\n screenshotResult = await apiRequest(`/api/preview/${previewId}/screenshots`, {\n ...opts,\n body: { clients, modes },\n timeoutMs: 120_000,\n });\n }\n\n // If screenshots still failed, return preview analysis with error note\n if (screenshotResult.isError) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n previewId,\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\n overallScore: previewResult.data!.overallScore,\n screenshots: {},\n screenshotStatus: \"failed\",\n captured: [],\n failed: [],\n note: `Preview created but screenshot capture failed: ${screenshotResult.error}. View the full preview at the URL above.`,\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n const ss = screenshotResult.data!;\n const result: Record<string, unknown> = {\n previewId,\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\n overallScore: previewResult.data!.overallScore,\n screenshots: ss.screenshots,\n screenshotStatus: ss.screenshotStatus,\n captured: ss.captured,\n failed: ss.failed,\n };\n\n if (ss.failed?.length > 0) {\n result.note = `${ss.failed.length} of ${(ss.captured?.length ?? 0) + ss.failed.length} screenshots failed: ${ss.failed.join(\", \")}`;\n }\n\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] };\n },\n);\n\n// ── Hosted Tool: share_preview ─────────────────────────────────────\n\nserver.registerTool(\n \"share_preview\",\n {\n title: \"Share Preview\",\n description:\n \"Create a shareable link for an email preview. Recipients see the full analysis without needing an account. Requires EMAILENS_API_KEY env var and Dev plan ($9/mo).\",\n inputSchema: {\n html: z.string().describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so\"),\n title: z.string().optional().describe(\"Display title for the share page\"),\n format: formatEnum.describe(\"Input format\"),\n },\n annotations: {\n title: \"Share Preview\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ html, title, format }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"share_preview\", \"Shareable links require the Dev plan ($9/mo).\");\n }\n\n const result = await apiRequest<{ id: string; expiresAt: string | null }>(\"/api/share\", {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n body: { html, title, format },\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n shareUrl: `${config.apiUrl}/share/${result.data!.id}`,\n expiresAt: result.data!.expiresAt,\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Hosted Tool: sandbox_get_inbox ──────────────────────────────────\n\nserver.registerTool(\n \"sandbox_get_inbox\",\n {\n title: \"Get Sandbox Inbox\",\n description:\n \"Get your active Emailens sandbox test address (or create a new one). Send emails here via SMTP (smtp.emailens.dev:2525) or inbound domain routing. Requires EMAILENS_API_KEY.\",\n inputSchema: {\n name: z.string().optional().describe(\"Optional inbox name (e.g. 'Auth E2E Test')\"),\n },\n annotations: {\n title: \"Get Sandbox Inbox\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ name }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"sandbox_get_inbox\", \"Email sandboxing requires an Emailens account and API key.\");\n }\n\n const result = await apiRequest<{\n inbox?: { id: string; name: string; createdAt: string; lastReceivedAt: string | null };\n address?: string;\n }>(\"/api/sandbox/v1/inbox\", {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data, null, 2),\n },\n ],\n };\n },\n);\n\n// ── Hosted Tool: sandbox_wait_for_email ─────────────────────────────\n\nserver.registerTool(\n \"sandbox_wait_for_email\",\n {\n title: \"Wait for Sandbox Email\",\n description:\n \"Poll and wait for a matching incoming test email in the sandbox. Automatically extracts 4-8 digit OTP codes and action links for zero-regex test assertions. Requires EMAILENS_API_KEY.\",\n inputSchema: {\n inboxId: z.string().optional().describe(\"Optional inbox ID filter\"),\n subject: z.string().optional().describe(\"Substring match for email subject (case-insensitive)\"),\n sender: z.string().optional().describe(\"Substring match for sender address (case-insensitive)\"),\n timeoutSeconds: z.number().optional().describe(\"Max seconds to wait (default: 30, max: 60)\"),\n },\n annotations: {\n title: \"Wait for Sandbox Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ inboxId, subject, sender, timeoutSeconds = 30 }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"sandbox_wait_for_email\", \"Email sandboxing requires an Emailens account and API key.\");\n }\n\n const params = new URLSearchParams();\n if (inboxId) params.set(\"inboxId\", inboxId);\n if (subject) params.set(\"subject\", subject);\n if (sender) params.set(\"sender\", sender);\n params.set(\"timeout\", String(timeoutSeconds));\n\n const result = await apiRequest<{\n message: {\n id: string;\n subject: string;\n sender: string;\n otp?: string | null;\n actionUrl?: string | null;\n links?: Array<{ href: string; text?: string }>;\n receivedAt: string;\n };\n }>(`/api/sandbox/v1/messages/wait?${params.toString()}`, {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n timeoutMs: (timeoutSeconds + 5) * 1000,\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data?.message ?? result.data, null, 2),\n },\n ],\n };\n },\n);\n\n// ── Hosted Tool: sandbox_list_messages ──────────────────────────────\n\nserver.registerTool(\n \"sandbox_list_messages\",\n {\n title: \"List Sandbox Messages\",\n description:\n \"List recently received test emails in your sandbox inboxes. Requires EMAILENS_API_KEY.\",\n inputSchema: {\n inboxId: z.string().optional().describe(\"Optional inbox ID filter\"),\n },\n annotations: {\n title: \"List Sandbox Messages\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ inboxId }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"sandbox_list_messages\", \"Email sandboxing requires an Emailens account and API key.\");\n }\n\n const params = inboxId ? `?inboxId=${encodeURIComponent(inboxId)}` : \"\";\n const result = await apiRequest<{ messages: unknown[] }>(`/api/sandbox/messages${params}`, {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data, null, 2),\n },\n ],\n };\n },\n);\n\n// ── Hosted Tool: sandbox_get_message ────────────────────────────────\n\nserver.registerTool(\n \"sandbox_get_message\",\n {\n title: \"Get Sandbox Message\",\n description:\n \"Get complete captured email details including HTML/text parts, extracted OTP, magic links, headers, and spam analysis. Requires EMAILENS_API_KEY.\",\n inputSchema: {\n messageId: z.string().describe(\"ID of the sandbox message\"),\n },\n annotations: {\n title: \"Get Sandbox Message\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ messageId }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"sandbox_get_message\", \"Email sandboxing requires an Emailens account and API key.\");\n }\n\n const result = await apiRequest<{ message: unknown }>(`/api/sandbox/messages/${encodeURIComponent(messageId)}`, {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data, null, 2),\n },\n ],\n };\n },\n);\n\n// ── Start ────────────────────────────────────────────────────────────\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n","import { EMAIL_CLIENTS, type CSSWarning, type SourceLocation } from \"@emailens/engine\";\r\n\r\n/**\r\n * Turning the engine's per-client warnings into something a model can afford\r\n * to read.\r\n *\r\n * The engine reports per client because a score is per client, and per selector\r\n * because a fix is per selector. On an ordinary 11KB newsletter that is 347\r\n * warnings, which collapse to **46 distinct property-and-severity pairs**.\r\n * `border-radius` alone arrives twelve times: two clients that drop it, six\r\n * selectors that use it, the same sentence and the same VML workaround in every\r\n * one. Serialised with the fix snippets that is 286KB, about 73,000 tokens, for\r\n * an email the size of a README.\r\n *\r\n * A tool that cannot be called twice in a conversation gets called once and\r\n * then avoided, so this collapses the duplication:\r\n *\r\n * - **One entry per property and severity**, listing the clients it affects.\r\n * The clients are the data; repeating the prose once per client is not.\r\n * - **The message loses the client's name**, which is the only thing that\r\n * differed between the copies. It is stripped by exact match against the\r\n * client's own display name rather than by pattern, so it cannot eat a real\r\n * word.\r\n * - **No `fix` snippets.** They were 93KB of the 286KB, and `fix_email` exists\r\n * precisely to produce them for the issues a caller decides to act on.\r\n * - **Positions merge.** Every place the property breaks, across every client\r\n * and selector, in document order.\r\n *\r\n * `detail: \"full\"` returns the engine's own per-client shape, unchanged, for a\r\n * caller that genuinely wants it.\r\n */\r\n\r\nconst CLIENT_NAMES = new Map(EMAIL_CLIENTS.map((c) => [c.id, c.name]));\r\n\r\n/** One problem, and everywhere it lands. */\r\nexport interface CollapsedFinding {\r\n property: string;\r\n severity: CSSWarning[\"severity\"];\r\n /** Client ids affected, in the matrix's own order. */\r\n clients: string[];\r\n /** The engine's message with the client's name removed. */\r\n message: string;\r\n suggestion?: string;\r\n /** `structural` means the fix changes markup, not just CSS. */\r\n fixType?: CSSWarning[\"fixType\"];\r\n /** True when `fix_email` can produce a snippet for this. */\r\n hasFix?: boolean;\r\n loc?: Pick<SourceLocation, \"line\" | \"column\" | \"offset\" | \"length\">;\r\n alsoAtLines?: number[];\r\n locsTruncated?: boolean;\r\n}\r\n\r\n/**\r\n * Strip the leading client name from a message.\r\n *\r\n * Messages read \"Outlook (New) does not support …\" / \"Yahoo Mail has partial\r\n * support for …\". Matching the client's own display name exactly is safer than\r\n * a pattern: \"Mail\" and \"Outlook\" appear inside real sentences.\r\n */\r\nfunction withoutClientName(message: string, clientId: string): string {\r\n const name = CLIENT_NAMES.get(clientId);\r\n if (!name || !message.startsWith(name)) return message;\r\n const rest = message.slice(name.length).trimStart();\r\n return rest ? rest.charAt(0).toUpperCase() + rest.slice(1) : message;\r\n}\r\n\r\n/** Positions from every warning in a group, deduplicated, in document order. */\r\nfunction mergeLocations(group: CSSWarning[]): SourceLocation[] {\r\n const byOffset = new Map<number, SourceLocation>();\r\n for (const w of group) {\r\n for (const loc of w.locs ?? (w.loc ? [w.loc] : [])) {\r\n if (!byOffset.has(loc.offset)) byOffset.set(loc.offset, loc);\r\n }\r\n }\r\n return [...byOffset.values()].sort((a, b) => a.offset - b.offset);\r\n}\r\n\r\n/**\r\n * Collapse per-client, per-selector warnings into one entry each.\r\n *\r\n * Grouped by property, severity **and message**: two clients can both drop a\r\n * property for different reasons, and merging those would attribute a caveat\r\n * to a client whose note never said it.\r\n */\r\nexport function collapseWarnings(warnings: CSSWarning[]): CollapsedFinding[] {\r\n const groups = new Map<string, CSSWarning[]>();\r\n for (const w of warnings) {\r\n // A NUL separator, because it cannot occur in a property, a severity or\r\n // a message; a space could, and two different triples would collide.\r\n const key = [w.property, w.severity, withoutClientName(w.message, w.client)].join(\"\\u0000\");\r\n const seen = groups.get(key);\r\n if (seen) seen.push(w);\r\n else groups.set(key, [w]);\r\n }\r\n\r\n return [...groups.values()].map((group) => {\r\n const first = group[0];\r\n const clients = [...new Set(group.map((w) => w.client))];\r\n const locs = mergeLocations(group);\r\n // A suggestion can differ per client (5 of 46 groups on the reference\r\n // fixture). Keeping the first would silently attribute one client's advice\r\n // to another, so a group that disagrees says nothing and lets the caller\r\n // ask `fix_email`.\r\n const suggestions = new Set(group.map((w) => w.suggestion ?? \"\"));\r\n const suggestion = suggestions.size === 1 ? first.suggestion : undefined;\r\n\r\n return {\r\n property: first.property,\r\n severity: first.severity,\r\n clients,\r\n message: withoutClientName(first.message, first.client),\r\n ...(suggestion ? { suggestion } : {}),\r\n ...(first.fixType ? { fixType: first.fixType } : {}),\r\n ...(group.some((w) => w.fix) ? { hasFix: true } : {}),\r\n ...(locs.length\r\n ? {\r\n loc: {\r\n line: locs[0].line,\r\n column: locs[0].column,\r\n offset: locs[0].offset,\r\n length: locs[0].length,\r\n },\r\n // Lines, not positions, and each line once: two occurrences on\r\n // one line are two offsets but one place to look.\r\n ...(() => {\r\n const lines = [...new Set(locs.slice(1).map((l) => l.line))].filter(\r\n (line) => line !== locs[0].line,\r\n );\r\n return lines.length ? { alsoAtLines: lines } : {};\r\n })(),\r\n }\r\n : {}),\r\n ...(group.some((w) => w.locsTruncated) ? { locsTruncated: true } : {}),\r\n };\r\n });\r\n}\r\n\r\n/** Keep only the warnings for these clients. An empty or absent list keeps all. */\r\nexport function forClients(warnings: CSSWarning[], clients?: string[]): CSSWarning[] {\r\n if (!clients?.length) return warnings;\r\n const wanted = new Set(clients);\r\n return warnings.filter((w) => wanted.has(w.client));\r\n}\r\n\r\n/** Every client id the engine knows, for validating a caller's filter. */\r\nexport function knownClientIds(): string[] {\r\n return EMAIL_CLIENTS.map((c) => c.id);\r\n}\r\n","import { compile } from \"@emailens/engine/compile\";\n\n/**\n * Getting a template to HTML before anything looks at it.\n *\n * The `format` parameter used to do nothing but choose the syntax of the fix\n * snippets, while the source was handed to the analyzer as-is. That is a quiet\n * and expensive lie: cheerio parses `<mjml><mj-section>…` without complaint,\n * finds no CSS in it, and the tools answer **zero findings**. An assistant\n * reads that as a clean bill of health and says the campaign is fine. A wrong\n * answer delivered confidently is worse than an error.\n *\n * So a non-HTML format is compiled first, with the same compilers the CLI uses.\n * They are optional peer dependencies of the engine (MJML alone is 56MB, and\n * this server is usually run through `npx`), so a caller can land on a machine\n * that has none of them. That case gets the engine's own \"install this\" message\n * plus the way out that needs nothing installed: compile it yourself and send\n * the HTML.\n */\n\nexport type SourceResult = { ok: true; html: string } | { ok: false; message: string };\n\nconst COMPILED: ReadonlySet<string> = new Set([\"jsx\", \"mjml\", \"maizzle\"]);\n\n/**\n * Is this \"the package isn't here\" rather than \"the template is wrong\"?\n *\n * The engine says `MJML compilation requires \"mjml\"` for a missing compiler\n * and `Sandbox strategy \"isolated-vm\" requires the \"isolated-vm\" package` when\n * the compiler is present but its sandbox is not, which is what React Email\n * hits first. Both are the same problem for the caller, so one pattern covers\n * them. Getting this wrong is cheap in one direction only: a misclassified\n * template error gains an irrelevant sentence, while a misclassified missing\n * package would tell someone to debug markup that is fine.\n */\nfunction isMissingCompiler(message: string): boolean {\n return /requires (?:the )?\"/.test(message);\n}\n\n/**\n * Compile `source` when `format` names a template language, or pass it through\n * when it is already HTML.\n */\nfunction isVueSfc(source: string): boolean {\n return /<template[\\s>]/i.test(source) && /<\\/template>/i.test(source);\n}\n\n/** An omitted format on a Vue SFC is maizzle. An explicit format is kept. */\nexport function resolveSourceFormat(source: string, format?: string): string {\n if (format) return format;\n return isVueSfc(source) ? \"maizzle\" : \"html\";\n}\n\nexport async function toHtml(source: string, format?: string): Promise<SourceResult> {\n const resolved = resolveSourceFormat(source, format);\n if (resolved === \"html\") return { ok: true, html: source };\n if (!COMPILED.has(resolved)) {\n return { ok: false, message: `Unknown format \"${resolved}\". Use html, jsx, mjml, or maizzle.` };\n }\n\n try {\n return { ok: true, html: await compile(source, resolved as \"jsx\" | \"mjml\" | \"maizzle\") };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (isMissingCompiler(message)) {\n return {\n ok: false,\n message:\n `${message}\\n\\nThat has to happen where this MCP server runs, which may not be somewhere ` +\n `you can install to. If it is not, compile the template yourself and send the resulting ` +\n `HTML with format \"html\".`,\n };\n }\n return { ok: false, message };\n }\n}\n","export const config = {\r\n get apiKey(): string | null {\r\n return process.env.EMAILENS_API_KEY ?? null;\r\n },\r\n get apiUrl(): string {\r\n // The app host, not the apex: emailens.dev redirects /api/* here, and a\r\n // redirect to another host drops the Authorization header on the way.\r\n return process.env.EMAILENS_API_URL ?? \"https://app.emailens.dev\";\r\n },\r\n get isHosted(): boolean {\r\n return this.apiKey !== null;\r\n },\r\n};\r\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 app.emailens.dev/dashboard/api?utm_source=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?utm_source=mcp\"\n : \"Access denied. Check your plan at emailens.dev/pricing?utm_source=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?utm_source=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?utm_source=mcp\",\n \"2. Create a key at app.emailens.dev/dashboard/api?utm_source=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/drafts.ts","../src/source.ts","../src/hosted.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { collapseWarnings, forClients, knownClientIds } from \"./findings.js\";\nimport { resolveSourceFormat, toHtml } from \"./compile.js\";\nimport {\n createSession,\n analyzeEmail,\n generateCompatibilityScore,\n simulateDarkMode,\n transformForAllClients,\n transformForClient,\n generateFixPrompt,\n estimateAiFixTokens,\n diffResults,\n toPlainText,\n checkDesignRetention,\n EMAIL_CLIENTS,\n MAX_HTML_SIZE,\n type Framework,\n type CSSWarning,\n type SourceLocation,\n} from \"@emailens/engine\";\n// Lazy-imported: @emailens/engine/server may not exist in all engine versions\nlet _checkDeliverability: ((domain: string) => Promise<unknown>) | null = null;\nimport { config } from \"./config.js\";\nimport { firstDraft } from \"./drafts.js\";\nimport { readSource, withoutSource } from \"./source.js\";\nimport { apiRequest, mcpError, noApiKeyError } from \"./hosted.js\";\n\nfunction toFramework(format?: string): Framework | undefined {\n if (format === \"jsx\" || format === \"mjml\" || format === \"maizzle\") return format;\n return undefined;\n}\n\n/** The headline is the worst client. A mean of 21 clients hides the one that fails. */\nfunction worstScore(scores: Record<string, { score: number }>): number {\n const values = Object.values(scores);\n if (values.length === 0) return 0;\n return Math.min(...values.map((s) => s.score));\n}\n\n/**\n * The language the source is written in.\n *\n * Anything but `html` is compiled before analysis and also selects the syntax\n * the fix snippets are written in.\n */\nconst formatEnum = z.enum([\"html\", \"jsx\", \"mjml\", \"maizzle\"]).optional();\n\n/**\n * How much compatibility detail to return.\n *\n * `summary` collapses the engine's per-client, per-selector warnings into one\n * entry per problem listing the clients it affects: the same information at a\n * fraction of the tokens. It also leaves out info findings, counting them\n * instead: they fall back safely, and on a typical email they are 35 of 37\n * entries, which buried the two warnings an agent had to fix. `full` is the\n * engine's own shape, per client, with fix snippets and info, for a caller that\n * wants everything.\n */\nconst detailEnum = z\n .enum([\"summary\", \"full\"])\n .optional()\n .describe(\n \"'summary' (default) returns one entry per error or warning, listing the clients it affects, and only counts info findings (safe fallbacks). 'full' returns one entry per client with fix snippets, info included.\",\n );\n\nconst htmlParam = z\n .string()\n .optional()\n .describe(\"The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so. Pass this or `path`.\");\n\nconst pathParam = z\n .string()\n .optional()\n .describe(\"Absolute path to the email source file (.html, .htm, .mjml, .jsx, .tsx, .vue), instead of `html`. Prefer it when the email is on disk: the source stays out of your context. The extension sets `format` unless you pass one.\");\n\nconst targetingPolicyEnum = z\n .enum([\"progressive\", \"strict\", \"lenient\"])\n .optional()\n .describe(\n \"'progressive' (default) suppresses compatibility false-positives inside client-targeted scopes and only flags deprecated or dangerous hacks. 'strict' treats targeting as non-standard and reports every hack. 'lenient' suppresses false positives and ignores non-fatal targeting warnings.\",\n );\n\nconst skipEnum = z.enum([\n \"spam\",\n \"links\",\n \"accessibility\",\n \"images\",\n \"compatibility\",\n \"inboxPreview\",\n \"size\",\n \"templateVariables\",\n \"overflow\",\n \"visual\",\n \"darkContrast\",\n \"mobileContrast\",\n \"design\",\n \"vml\",\n \"styleSurvival\",\n \"targeting\",\n]);\n\nconst clientsParam = z\n .array(z.string())\n .optional()\n .describe(\n \"Only report these client IDs (e.g. ['gmail-web','outlook-windows']). The fastest way to cut the response when you care about specific clients. Use list_clients for the IDs.\",\n );\n\n/**\n * Reject a client id the engine does not know.\n *\n * Filtering to a typo would return an empty list, which reads as \"this email\n * is fine for that client\"; the most misleading answer the tool can give.\n */\nfunction validateClients(clients: string[] | undefined) {\n if (!clients?.length) return undefined;\n const known = new Set(knownClientIds());\n const unknown = clients.filter((c) => !known.has(c));\n if (!unknown.length) return undefined;\n return mcpError(\n `Unknown client ${unknown.length > 1 ? \"IDs\" : \"ID\"}: ${unknown.join(\", \")}. Call list_clients for the valid IDs.`,\n );\n}\n\n/** The compatibility payload, at the requested level of detail. */\nfunction compatibilityPayload(\n warnings: CSSWarning[],\n detail: \"summary\" | \"full\" | undefined,\n clients: string[] | undefined,\n) {\n const scoped = forClients(warnings, clients);\n if (detail === \"full\") {\n return {\n warningCount: scoped.length,\n warnings: scoped.map((w) => ({\n client: w.client,\n property: w.property,\n severity: w.severity,\n message: w.message,\n suggestion: w.suggestion,\n fix: w.fix,\n fixType: w.fixType,\n ...withPositions(w),\n })),\n };\n }\n const collapsed = collapseWarnings(scoped);\n const findings = collapsed.filter((f) => f.severity !== \"info\");\n const info = collapsed.length - findings.length;\n return {\n warningCount: scoped.length,\n findingCount: findings.length,\n findings,\n ...(info ? { infoFindings: info, infoNote: \"Safe fallbacks, not problems. Pass detail:'full' to list them.\" } : {}),\n };\n}\n\n/**\n * Do source positions refer to the code the caller handed us?\n *\n * Only for plain HTML. JSX, MJML and Maizzle are compiled before analysis, so a\n * position would point into generated output: worse than none, because an\n * agent would edit the wrong line with confidence.\n */\nfunction positionsApply(format?: string): boolean {\n return (format ?? \"html\") === \"html\";\n}\n\n/**\n * The position fields a finding carries, when it has them.\n *\n * Deliberately compact: this response is read by a model paying for every\n * token, and a newsletter can produce a thousand occurrences. The first one\n * gets a full position, enough to edit it, and the rest are reduced to line\n * numbers, which is all an assistant needs to go find them. Carrying every\n * occurrence in full nearly doubled the payload on a real fixture.\n */\nfunction withPositions<T extends {\n loc?: SourceLocation;\n locs?: SourceLocation[];\n locsTruncated?: boolean;\n}>(w: T) {\n if (!w.loc) return {};\n const { line, column, offset, length } = w.loc;\n const others = (w.locs ?? []).slice(1).map((l) => l.line);\n return {\n loc: { line, column, offset, length },\n ...(others.length ? { alsoAtLines: others } : {}),\n ...(w.locsTruncated ? { locsTruncated: true } : {}),\n };\n}\n\nfunction validateHtmlSize(html: string) {\n if (html.length > MAX_HTML_SIZE) {\n return mcpError(`HTML input exceeds ${Math.round(MAX_HTML_SIZE / 1024)}KB limit. Reduce the email size and try again.`);\n }\n return null;\n}\n\nconst server = new McpServer({\n name: \"emailens\",\n version: \"0.13.0\",\n});\n\n// ── Local Tool: preview_email ──────────────────────────────────────\n\nserver.registerTool(\n \"preview_email\",\n {\n title: \"Preview Email\",\n description:\n `Full email compatibility preview, transforms HTML for ${EMAIL_CLIENTS.length} email clients (Gmail, Outlook, Apple Mail, Yahoo, Samsung, Thunderbird, HEY, Proton Mail, AOL, Fastmail, Superhuman), analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.`,\n inputSchema: {\n html: htmlParam,\n path: pathParam,\n clients: z\n .array(z.string())\n .optional()\n .describe(\"Optional client ID filter (e.g. ['gmail-web', 'outlook-windows'])\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n targetingPolicy: targetingPolicyEnum,\n },\n annotations: {\n title: \"Preview Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html: inline, path, clients, format: given, targetingPolicy }) => {\n const input = await readSource(inline, path, given);\n if (!input.ok) return mcpError(input.message);\n const { html, format } = input;\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const validClientIds = new Set(EMAIL_CLIENTS.map((c) => c.id));\n const framework = toFramework(resolveSourceFormat(html, format));\n const session = createSession(source.html, { framework, targetingPolicy });\n\n let transforms;\n if (clients) {\n const filter = clients.filter((c) => validClientIds.has(c));\n if (filter.length === 0) {\n return mcpError(\n JSON.stringify({ error: \"No valid client IDs provided\", validClientIds: Array.from(validClientIds) }),\n );\n }\n transforms = filter.map((c) => transformForClient(source.html, c, framework));\n } else {\n transforms = transformForAllClients(source.html, framework);\n }\n\n const warnings = session.analyze();\n const scores = generateCompatibilityScore(warnings);\n const inboxPreview = session.extractInboxPreview();\n const sizeReport = session.checkSize();\n\n const darkMode: Record<string, { html: string; warnings: CSSWarning[] }> = {};\n for (const t of transforms) {\n darkMode[t.clientId] = simulateDarkMode(source.html, t.clientId);\n }\n\n const overallScore = worstScore(scores);\n\n const plainText = toPlainText(source.html);\n\n const result: Record<string, unknown> = {\n overallScore,\n compatibilityScores: scores,\n cssWarnings: warnings.map((w) => ({\n client: w.client,\n property: w.property,\n severity: w.severity,\n message: w.message,\n suggestion: w.suggestion,\n fix: w.fix,\n fixType: w.fixType,\n })),\n inboxPreview,\n sizeReport,\n ...(plainText ? { plainText } : {}),\n clientCount: transforms.length,\n darkModeWarnings: Object.entries(darkMode).reduce(\n (acc, [clientId, dm]) => {\n if (dm.warnings.length > 0) acc[clientId] = dm.warnings.length;\n return acc;\n },\n {} as Record<string, number>,\n ),\n };\n\n // Contextual tip when no API key: the fix route on issues, and on a clean\n // result the name of who checked it, so success is attributable too.\n if (!config.isHosted) {\n result.tip = overallScore < 90\n ? \"Capture real screenshots to verify these issues across clients. Add EMAILENS_API_KEY to your MCP config \\u2192 app.emailens.dev/dashboard/api?utm_source=mcp\"\n : \"Checked by Emailens against 21 email clients. Confirm on real Outlook, Gmail and Apple Mail \\u2192 emailens.dev/real-devices?utm_source=mcp\";\n }\n\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result) }] };\n },\n);\n\n// ── Local Tool: analyze_email (CSS-only, reverted from audit) ──────\n\nserver.registerTool(\n \"analyze_email\",\n {\n title: \"Analyze Email\",\n description:\n \"Fix error and warning findings. Info findings are not problems (the email falls back safely there): never remove responsive (@media) or dark-mode CSS to clear them. Quick CSS compatibility analysis, returns per-client scores and one finding per problem, listing the clients it affects. For HTML input each finding carries loc (line, column, offset) and alsoAtLines, so you can edit the exact source. Narrow with clients, or pass detail:'full' for the per-client breakdown with fix snippets. Use audit_email for a full quality report (spam, links, a11y, images, etc.).\",\n inputSchema: {\n html: htmlParam,\n path: pathParam,\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n detail: detailEnum,\n clients: clientsParam,\n targetingPolicy: targetingPolicyEnum,\n },\n annotations: {\n title: \"Analyze Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html: inline, path, format: given, detail, clients, targetingPolicy }) => {\n const input = await readSource(inline, path, given);\n if (!input.ok) return mcpError(input.message);\n const { html, format } = input;\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n const clientError = validateClients(clients);\n if (clientError) return clientError;\n\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const warnings = analyzeEmail(source.html, toFramework(resolveSourceFormat(html, format)), {\n positions: positionsApply(resolveSourceFormat(html, format)),\n targetingPolicy,\n });\n // Scores stay whole-email: filtering to two clients should narrow what is\n // reported, not silently change what the email scores.\n const scores = generateCompatibilityScore(warnings);\n\n const overallScore = worstScore(scores);\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n overallScore,\n scores,\n ...compatibilityPayload(warnings, detail, clients),\n }),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: audit_email ────────────────────────────────────────\n\nserver.registerTool(\n \"audit_email\",\n {\n title: \"Audit Email\",\n description:\n \"Fix error and warning findings, and restore anything listed in designRemovals. Info findings are not problems (the email falls back safely there): never remove responsive (@media), dark-mode CSS or rounded corners to clear them. designRemovals compares this version with the first one audited for the same email (or with previousHtml): design the edits deleted, such as @media rules, dark mode, corners, gradients, VML fallbacks or images. Comprehensive email quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual fallbacks, dark-mode and mobile text contrast, design consistency, structural faults in Outlook-only VML (which lives inside conditional comments and is invisible to every other check), style survival: CSS a client parses correctly and then discards, such as a stylesheet past Gmail's 16 KB ceiling or every rule after a `}}`, which no support matrix can express, and client targeting: HowToTarget hacks such as Gmail u+.body or MSO conditionals, which do not score as css-hack. For HTML input, findings tied to a specific element carry loc (line, column, offset) so you can edit the exact source. Compatibility is collapsed to one finding per problem listing the clients it affects; pass detail:'full' for the per-client breakdown with fix snippets. Use skip to omit checks, targetingPolicy to change how targeted scopes affect scores, and clients to narrow which clients are reported.\",\n inputSchema: {\n html: htmlParam,\n path: pathParam,\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n skip: z\n .array(skipEnum)\n .optional()\n .describe(\"Checks to skip (e.g. ['spam', 'images'])\"),\n detail: detailEnum,\n clients: clientsParam,\n targetingPolicy: targetingPolicyEnum,\n emailType: z\n .enum([\"marketing\", \"transactional\"])\n .optional()\n .describe(\"'transactional' (receipts, password resets, codes, alerts: no unsubscribe link required) or 'marketing'. Omitted: guessed from the content, which misses many transactional emails.\"),\n previousHtml: z\n .string()\n .optional()\n .describe(\"The design to hold this version to, usually your first draft. Omitted: the first version of this email audited in this session (matched by <title> and text).\"),\n },\n annotations: {\n title: \"Audit Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html: inline, path, format: given, skip, detail, clients, targetingPolicy, emailType, previousHtml }) => {\n const input = await readSource(inline, path, given);\n if (!input.ok) return mcpError(input.message);\n const { html, format } = input;\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n const clientError = validateClients(clients);\n if (clientError) return clientError;\n\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const session = createSession(source.html, {\n framework: toFramework(resolveSourceFormat(html, format)),\n positions: positionsApply(resolveSourceFormat(html, format)),\n targetingPolicy,\n });\n const report = session.audit({ skip, ...(emailType ? { spam: { emailType } } : {}) });\n\n const overallCompatibility = worstScore(report.compatibility.scores);\n const baseline = previousHtml ? (await toHtml(previousHtml, format)) : { ok: true as const, html: firstDraft(source.html) };\n const designRemovals = baseline.ok ? checkDesignRetention(baseline.html, source.html) : [];\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n overallCompatibility,\n designRemovals,\n compatibility: {\n scores: report.compatibility.scores,\n ...compatibilityPayload(report.compatibility.warnings, detail, clients),\n },\n spam: report.spam,\n links: report.links,\n accessibility: report.accessibility,\n images: report.images,\n inboxPreview: report.inboxPreview,\n size: report.size,\n templateVariables: report.templateVariables,\n overflow: report.overflow,\n visual: report.visual,\n vml: report.vml,\n darkContrast: report.darkContrast,\n mobileContrast: report.mobileContrast,\n design: report.design,\n styleSurvival: report.styleSurvival,\n targeting: report.targeting,\n }),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: fix_email ──────────────────────────────────────────\n\nserver.registerTool(\n \"fix_email\",\n {\n title: \"Fix Email\",\n description:\n \"Generate a structured fix prompt for email compatibility issues. Returns markdown with the original code, detected issues (CSS or structural), fix snippets, and format-specific instructions. Use after preview_email or analyze_email.\",\n inputSchema: {\n html: htmlParam,\n path: pathParam,\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n scope: z.enum([\"all\", \"current\"]).optional().describe(\"'all' (default) or 'current' (requires selectedClientId)\"),\n selectedClientId: z.string().optional().describe(\"Client ID to scope fixes to (e.g. 'outlook-windows')\"),\n },\n annotations: {\n title: \"Fix Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ html: inline, path, format: given, scope, selectedClientId }) => {\n const input = await readSource(inline, path, given);\n if (!input.ok) return mcpError(input.message);\n const { html, format } = input;\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n\n // The findings come from the compiled output, because that is what a\n // client renders. The prompt keeps the caller's own source, because that is\n // the file they will edit; a fix written against generated HTML is not a\n // fix they can apply.\n const source = await toHtml(html, format);\n if (!source.ok) return mcpError(source.message);\n\n const resolved = resolveSourceFormat(html, format) as\n | \"html\"\n | \"jsx\"\n | \"mjml\"\n | \"maizzle\";\n const framework = toFramework(resolved);\n const fixScope = scope === \"current\" ? \"current\" : \"all\";\n const inputFormat = resolved;\n\n const warnings = analyzeEmail(source.html, framework);\n const scores = generateCompatibilityScore(warnings);\n\n if (warnings.length === 0) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify({ message: \"No compatibility issues found \\u2014 nothing to fix.\", overallScore: 100 }),\n },\n ],\n };\n }\n\n const estimate = await estimateAiFixTokens({\n originalHtml: html,\n warnings,\n scores,\n scope: fixScope,\n selectedClientId,\n format: inputFormat,\n });\n\n const prompt = generateFixPrompt({\n originalHtml: html,\n warnings,\n scores,\n scope: fixScope,\n selectedClientId,\n format: inputFormat,\n });\n\n const structural = warnings.filter((w) => w.fixType === \"structural\");\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n totalWarnings: warnings.length,\n structuralWarnings: structural.length,\n cssWarnings: warnings.length - structural.length,\n tokenEstimate: {\n inputTokens: estimate.inputTokens,\n estimatedOutputTokens: estimate.estimatedOutputTokens,\n truncated: estimate.truncated,\n },\n note: \"Apply the fixes in the prompt below. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes.\",\n }),\n },\n { type: \"text\" as const, text: input.path ? withoutSource(prompt, html, inputFormat, input.path) : prompt },\n ],\n };\n },\n);\n\n// ── Local Tool: list_clients ───────────────────────────────────────\n\nserver.registerTool(\n \"list_clients\",\n {\n title: \"List Email Clients\",\n description: \"List every supported email client with IDs, names, engines, and dark mode support.\",\n annotations: {\n title: \"List Email Clients\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async () => {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n EMAIL_CLIENTS.map((c) => ({\n id: c.id,\n name: c.name,\n category: c.category,\n engine: c.engine,\n darkModeSupport: c.darkModeSupport,\n ...(c.deprecated ? { deprecated: c.deprecated } : {}),\n }))),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: diff_emails ────────────────────────────────────────\n\nserver.registerTool(\n \"diff_emails\",\n {\n title: \"Diff Emails\",\n description:\n \"Compare two email HTML versions, shows score changes, fixed issues, and newly introduced issues per client. Use after making fixes to verify improvements.\",\n inputSchema: {\n before: z.string().optional().describe(\"Original email source. Pass this or `beforePath`.\"),\n after: z.string().optional().describe(\"Modified email source, in the same format. Pass this or `afterPath`.\"),\n beforePath: z.string().optional().describe(\"Absolute path to the original source file, instead of `before`\"),\n afterPath: z.string().optional().describe(\"Absolute path to the modified source file, instead of `after`\"),\n format: formatEnum.describe(\"Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.\"),\n },\n annotations: {\n title: \"Diff Emails\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async ({ before: beforeInline, after: afterInline, beforePath, afterPath, format: given }) => {\n const beforeInput = await readSource(beforeInline, beforePath, given, { html: \"before\", path: \"beforePath\" });\n if (!beforeInput.ok) return mcpError(beforeInput.message);\n const afterInput = await readSource(afterInline, afterPath, given, { html: \"after\", path: \"afterPath\" });\n if (!afterInput.ok) return mcpError(afterInput.message);\n const before = beforeInput.html;\n const after = afterInput.html;\n const format = afterInput.format ?? beforeInput.format;\n const beforeSizeError = validateHtmlSize(before);\n if (beforeSizeError) return beforeSizeError;\n const afterSizeError = validateHtmlSize(after);\n if (afterSizeError) return afterSizeError;\n\n const beforeSource = await toHtml(before, format);\n if (!beforeSource.ok) return mcpError(beforeSource.message);\n const afterSource = await toHtml(after, format);\n if (!afterSource.ok) return mcpError(afterSource.message);\n\n const framework = toFramework(\n resolveSourceFormat(before, format) === \"maizzle\" ||\n resolveSourceFormat(after, format) === \"maizzle\"\n ? (format ?? \"maizzle\")\n : (format ?? \"html\"),\n );\n\n const beforeWarnings = analyzeEmail(beforeSource.html, framework);\n const beforeScores = generateCompatibilityScore(beforeWarnings);\n\n const afterWarnings = analyzeEmail(afterSource.html, framework);\n const afterScores = generateCompatibilityScore(afterWarnings);\n\n const results = diffResults(\n { scores: beforeScores, warnings: beforeWarnings },\n { scores: afterScores, warnings: afterWarnings },\n );\n\n let clientsImproved = 0;\n let clientsRegressed = 0;\n let clientsUnchanged = 0;\n let totalDelta = 0;\n\n const mappedResults = results.map((r) => {\n if (r.scoreDelta > 0) clientsImproved++;\n else if (r.scoreDelta < 0) clientsRegressed++;\n else clientsUnchanged++;\n totalDelta += r.scoreDelta;\n\n return {\n clientId: r.clientId,\n scoreBefore: r.scoreBefore,\n scoreAfter: r.scoreAfter,\n scoreDelta: r.scoreDelta,\n fixed: r.fixed.map((w) => ({ property: w.property, client: w.client, message: w.message })),\n introduced: r.introduced.map((w) => ({ property: w.property, client: w.client, message: w.message })),\n unchanged: r.unchanged.length,\n };\n });\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n results: mappedResults,\n summary: {\n clientsImproved,\n clientsRegressed,\n clientsUnchanged,\n avgScoreDelta: results.length > 0 ? Math.round((totalDelta / results.length) * 10) / 10 : 0,\n },\n }),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: check_deliverability ───────────────────────────────\n\nserver.registerTool(\n \"check_deliverability\",\n {\n title: \"Check Deliverability\",\n description:\n \"Check email deliverability for a domain: SPF, DKIM, DMARC, MX, and BIMI records. Returns a score (0-100) and actionable issues. Uses DNS lookups (no API key needed).\",\n inputSchema: {\n domain: z\n .string()\n .min(3)\n .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, \"Invalid domain format\")\n .describe(\"Domain to check (e.g. 'company.com')\"),\n },\n annotations: {\n title: \"Check Deliverability\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ domain }) => {\n try {\n if (!_checkDeliverability) {\n const mod = await import(\"@emailens/engine/server\");\n _checkDeliverability = mod.checkDeliverability;\n }\n const report = await _checkDeliverability!(domain);\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(report) }],\n };\n } catch (err) {\n const msg = (err as Error).message ?? String(err);\n if (msg.includes(\"Cannot find\") || msg.includes(\"cannot find\") || msg.includes(\"MODULE_NOT_FOUND\")) {\n return mcpError(\n \"check_deliverability requires @emailens/engine >=0.9.1 with the /server subpath. Update with: npm install @emailens/engine@latest\",\n );\n }\n return mcpError(`Deliverability check failed for \"${domain}\": ${msg}`);\n }\n },\n);\n\n// ── Hosted Tool: capture_screenshots ───────────────────────────────\n\nserver.registerTool(\n \"capture_screenshots\",\n {\n title: \"Capture Screenshots\",\n description:\n \"Capture real email screenshots across 21 clients (Gmail, Outlook, Apple Mail, etc.) with light and dark mode variants. Screenshots are rendered in real browsers and hosted on CDN. Requires EMAILENS_API_KEY env var \\u2014 free plan at emailens.dev?utm_source=mcp.\",\n inputSchema: {\n html: htmlParam,\n path: pathParam,\n format: formatEnum.describe(\"Input format\"),\n clients: z.array(z.string()).optional().describe(\"Filter to specific client IDs\"),\n modes: z\n .array(z.enum([\"light\", \"dark\"]))\n .optional()\n .describe(\"Screenshot modes (default: ['light'])\"),\n title: z.string().optional().describe(\"Name for the hosted preview\"),\n },\n annotations: {\n title: \"Capture Screenshots\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ html: inline, path, format: given, clients, modes, title }) => {\n const input = await readSource(inline, path, given);\n if (!input.ok) return mcpError(input.message);\n const { html, format } = input;\n if (!config.isHosted) {\n return noApiKeyError(\"capture_screenshots\");\n }\n\n const opts = { apiKey: config.apiKey!, apiUrl: config.apiUrl };\n\n // Step 1: Create hosted preview\n const previewResult = await apiRequest<{\n id: string;\n overallScore: number;\n }>(\"/api/preview\", {\n ...opts,\n body: { html, format, clients, title, source: \"mcp\" },\n timeoutMs: 30_000,\n });\n\n if (previewResult.isError) {\n return mcpError(previewResult.error!);\n }\n\n const previewId = previewResult.data!.id;\n\n // Step 2: Capture screenshots (with one retry on timeout)\n let screenshotResult = await apiRequest<{\n screenshots: Record<string, string>;\n screenshotStatus: string;\n captured: string[];\n failed: string[];\n }>(`/api/preview/${previewId}/screenshots`, {\n ...opts,\n body: { clients, modes },\n timeoutMs: 120_000,\n });\n\n // Retry once on timeout\n if (screenshotResult.isError && screenshotResult.error?.includes(\"timed out\")) {\n screenshotResult = await apiRequest(`/api/preview/${previewId}/screenshots`, {\n ...opts,\n body: { clients, modes },\n timeoutMs: 120_000,\n });\n }\n\n // If screenshots still failed, return preview analysis with error note\n if (screenshotResult.isError) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n previewId,\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\n overallScore: previewResult.data!.overallScore,\n screenshots: {},\n screenshotStatus: \"failed\",\n captured: [],\n failed: [],\n note: `Preview created but screenshot capture failed: ${screenshotResult.error}. View the full preview at the URL above.`,\n }),\n },\n ],\n };\n }\n\n const ss = screenshotResult.data!;\n const result: Record<string, unknown> = {\n previewId,\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\n overallScore: previewResult.data!.overallScore,\n screenshots: ss.screenshots,\n screenshotStatus: ss.screenshotStatus,\n captured: ss.captured,\n failed: ss.failed,\n };\n\n if (ss.failed?.length > 0) {\n result.note = `${ss.failed.length} of ${(ss.captured?.length ?? 0) + ss.failed.length} screenshots failed: ${ss.failed.join(\", \")}`;\n }\n\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result) }] };\n },\n);\n\n// ── Hosted Tool: share_preview ─────────────────────────────────────\n\nserver.registerTool(\n \"share_preview\",\n {\n title: \"Share Preview\",\n description:\n \"Create a shareable link for an email preview. Recipients see the full analysis without needing an account. Requires EMAILENS_API_KEY env var and Dev plan ($9/mo).\",\n inputSchema: {\n html: htmlParam,\n path: pathParam,\n title: z.string().optional().describe(\"Display title for the share page\"),\n format: formatEnum.describe(\"Input format\"),\n },\n annotations: {\n title: \"Share Preview\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ html: inline, path, title, format: given }) => {\n const input = await readSource(inline, path, given);\n if (!input.ok) return mcpError(input.message);\n const { html, format } = input;\n if (!config.isHosted) {\n return noApiKeyError(\"share_preview\", \"Shareable links require the Dev plan ($9/mo).\");\n }\n\n const result = await apiRequest<{ id: string; expiresAt: string | null }>(\"/api/share\", {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n body: { html, title, format },\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n shareUrl: `${config.apiUrl}/share/${result.data!.id}`,\n expiresAt: result.data!.expiresAt,\n }),\n },\n ],\n };\n },\n);\n\n// ── Hosted Tool: sandbox_get_inbox ──────────────────────────────────\n\nserver.registerTool(\n \"sandbox_get_inbox\",\n {\n title: \"Get Sandbox Inbox\",\n description:\n \"Get your active Emailens sandbox test address (or create a new one). Send emails here via SMTP (smtp.emailens.dev:2525) or inbound domain routing. Requires EMAILENS_API_KEY.\",\n inputSchema: {\n name: z.string().optional().describe(\"Optional inbox name (e.g. 'Auth E2E Test')\"),\n },\n annotations: {\n title: \"Get Sandbox Inbox\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ name }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"sandbox_get_inbox\", \"Email sandboxing requires an Emailens account and API key.\");\n }\n\n const result = await apiRequest<{\n inbox?: { id: string; name: string; createdAt: string; lastReceivedAt: string | null };\n address?: string;\n }>(\"/api/sandbox/v1/inbox\", {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data),\n },\n ],\n };\n },\n);\n\n// ── Hosted Tool: sandbox_wait_for_email ─────────────────────────────\n\nserver.registerTool(\n \"sandbox_wait_for_email\",\n {\n title: \"Wait for Sandbox Email\",\n description:\n \"Poll and wait for a matching incoming test email in the sandbox. Automatically extracts 4-8 digit OTP codes and action links for zero-regex test assertions. Requires EMAILENS_API_KEY.\",\n inputSchema: {\n inboxId: z.string().optional().describe(\"Optional inbox ID filter\"),\n subject: z.string().optional().describe(\"Substring match for email subject (case-insensitive)\"),\n sender: z.string().optional().describe(\"Substring match for sender address (case-insensitive)\"),\n timeoutSeconds: z.number().optional().describe(\"Max seconds to wait (default: 30, max: 60)\"),\n },\n annotations: {\n title: \"Wait for Sandbox Email\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ inboxId, subject, sender, timeoutSeconds = 30 }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"sandbox_wait_for_email\", \"Email sandboxing requires an Emailens account and API key.\");\n }\n\n const params = new URLSearchParams();\n if (inboxId) params.set(\"inboxId\", inboxId);\n if (subject) params.set(\"subject\", subject);\n if (sender) params.set(\"sender\", sender);\n params.set(\"timeout\", String(timeoutSeconds));\n\n const result = await apiRequest<{\n message: {\n id: string;\n subject: string;\n sender: string;\n otp?: string | null;\n actionUrl?: string | null;\n links?: Array<{ href: string; text?: string }>;\n receivedAt: string;\n };\n }>(`/api/sandbox/v1/messages/wait?${params.toString()}`, {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n timeoutMs: (timeoutSeconds + 5) * 1000,\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data?.message ?? result.data),\n },\n ],\n };\n },\n);\n\n// ── Hosted Tool: sandbox_list_messages ──────────────────────────────\n\nserver.registerTool(\n \"sandbox_list_messages\",\n {\n title: \"List Sandbox Messages\",\n description:\n \"List recently received test emails in your sandbox inboxes. Requires EMAILENS_API_KEY.\",\n inputSchema: {\n inboxId: z.string().optional().describe(\"Optional inbox ID filter\"),\n },\n annotations: {\n title: \"List Sandbox Messages\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ inboxId }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"sandbox_list_messages\", \"Email sandboxing requires an Emailens account and API key.\");\n }\n\n const params = inboxId ? `?inboxId=${encodeURIComponent(inboxId)}` : \"\";\n const result = await apiRequest<{ messages: unknown[] }>(`/api/sandbox/messages${params}`, {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data),\n },\n ],\n };\n },\n);\n\n// ── Hosted Tool: sandbox_get_message ────────────────────────────────\n\nserver.registerTool(\n \"sandbox_get_message\",\n {\n title: \"Get Sandbox Message\",\n description:\n \"Get complete captured email details including HTML/text parts, extracted OTP, magic links, headers, and spam analysis. Requires EMAILENS_API_KEY.\",\n inputSchema: {\n messageId: z.string().describe(\"ID of the sandbox message\"),\n },\n annotations: {\n title: \"Get Sandbox Message\",\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ messageId }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"sandbox_get_message\", \"Email sandboxing requires an Emailens account and API key.\");\n }\n\n const result = await apiRequest<{ message: unknown }>(`/api/sandbox/messages/${encodeURIComponent(messageId)}`, {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(result.data),\n },\n ],\n };\n },\n);\n\n// ── Start ────────────────────────────────────────────────────────────\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n","import { EMAIL_CLIENTS, type CSSWarning, type SourceLocation } from \"@emailens/engine\";\r\n\r\n/**\r\n * Turning the engine's per-client warnings into something a model can afford\r\n * to read.\r\n *\r\n * The engine reports per client because a score is per client, and per selector\r\n * because a fix is per selector. On an ordinary 11KB newsletter that is 347\r\n * warnings, which collapse to **46 distinct property-and-severity pairs**.\r\n * `border-radius` alone arrives twelve times: two clients that drop it, six\r\n * selectors that use it, the same sentence and the same VML workaround in every\r\n * one. Serialised with the fix snippets that is 286KB, about 73,000 tokens, for\r\n * an email the size of a README.\r\n *\r\n * A tool that cannot be called twice in a conversation gets called once and\r\n * then avoided, so this collapses the duplication:\r\n *\r\n * - **One entry per property and severity**, listing the clients it affects.\r\n * The clients are the data; repeating the prose once per client is not.\r\n * - **The message loses the client's name**, which is the only thing that\r\n * differed between the copies. It is stripped by exact match against the\r\n * client's own display name rather than by pattern, so it cannot eat a real\r\n * word.\r\n * - **No `fix` snippets.** They were 93KB of the 286KB, and `fix_email` exists\r\n * precisely to produce them for the issues a caller decides to act on.\r\n * - **Positions merge.** Every place the property breaks, across every client\r\n * and selector, in document order.\r\n *\r\n * `detail: \"full\"` returns the engine's own per-client shape, unchanged, for a\r\n * caller that genuinely wants it.\r\n */\r\n\r\nconst CLIENT_NAMES = new Map(EMAIL_CLIENTS.map((c) => [c.id, c.name]));\r\n\r\n/** One problem, and everywhere it lands. */\r\nexport interface CollapsedFinding {\r\n property: string;\r\n severity: CSSWarning[\"severity\"];\r\n /** Client ids affected, in the matrix's own order. */\r\n clients: string[];\r\n /** The engine's message with the client's name removed. */\r\n message: string;\r\n suggestion?: string;\r\n /** `structural` means the fix changes markup, not just CSS. */\r\n fixType?: CSSWarning[\"fixType\"];\r\n /** True when `fix_email` can produce a snippet for this. */\r\n hasFix?: boolean;\r\n loc?: Pick<SourceLocation, \"line\" | \"column\" | \"offset\" | \"length\">;\r\n alsoAtLines?: number[];\r\n locsTruncated?: boolean;\r\n}\r\n\r\n/**\r\n * Strip the leading client name from a message.\r\n *\r\n * Messages read \"Outlook (New) does not support …\" / \"Yahoo Mail has partial\r\n * support for …\". Matching the client's own display name exactly is safer than\r\n * a pattern: \"Mail\" and \"Outlook\" appear inside real sentences.\r\n */\r\nfunction withoutClientName(message: string, clientId: string): string {\r\n const name = CLIENT_NAMES.get(clientId);\r\n if (!name || !message.startsWith(name)) return message;\r\n const rest = message.slice(name.length).trimStart();\r\n return rest ? rest.charAt(0).toUpperCase() + rest.slice(1) : message;\r\n}\r\n\r\n/** Positions from every warning in a group, deduplicated, in document order. */\r\nfunction mergeLocations(group: CSSWarning[]): SourceLocation[] {\r\n const byOffset = new Map<number, SourceLocation>();\r\n for (const w of group) {\r\n for (const loc of w.locs ?? (w.loc ? [w.loc] : [])) {\r\n if (!byOffset.has(loc.offset)) byOffset.set(loc.offset, loc);\r\n }\r\n }\r\n return [...byOffset.values()].sort((a, b) => a.offset - b.offset);\r\n}\r\n\r\n/**\r\n * Collapse per-client, per-selector warnings into one entry each.\r\n *\r\n * Grouped by property, severity **and message**: two clients can both drop a\r\n * property for different reasons, and merging those would attribute a caveat\r\n * to a client whose note never said it.\r\n */\r\nexport function collapseWarnings(warnings: CSSWarning[]): CollapsedFinding[] {\r\n const groups = new Map<string, CSSWarning[]>();\r\n for (const w of warnings) {\r\n // A NUL separator, because it cannot occur in a property, a severity or\r\n // a message; a space could, and two different triples would collide.\r\n const key = [w.property, w.severity, withoutClientName(w.message, w.client)].join(\"\\u0000\");\r\n const seen = groups.get(key);\r\n if (seen) seen.push(w);\r\n else groups.set(key, [w]);\r\n }\r\n\r\n return [...groups.values()].map((group) => {\r\n const first = group[0];\r\n const clients = [...new Set(group.map((w) => w.client))];\r\n const locs = mergeLocations(group);\r\n // A suggestion can differ per client (5 of 46 groups on the reference\r\n // fixture). Keeping the first would silently attribute one client's advice\r\n // to another, so a group that disagrees says nothing and lets the caller\r\n // ask `fix_email`.\r\n const suggestions = new Set(group.map((w) => w.suggestion ?? \"\"));\r\n const suggestion = suggestions.size === 1 ? first.suggestion : undefined;\r\n\r\n return {\r\n property: first.property,\r\n severity: first.severity,\r\n clients,\r\n message: withoutClientName(first.message, first.client),\r\n ...(suggestion ? { suggestion } : {}),\r\n ...(first.fixType ? { fixType: first.fixType } : {}),\r\n ...(group.some((w) => w.fix) ? { hasFix: true } : {}),\r\n ...(locs.length\r\n ? {\r\n loc: {\r\n line: locs[0].line,\r\n column: locs[0].column,\r\n offset: locs[0].offset,\r\n length: locs[0].length,\r\n },\r\n // Lines, not positions, and each line once: two occurrences on\r\n // one line are two offsets but one place to look.\r\n ...(() => {\r\n const lines = [...new Set(locs.slice(1).map((l) => l.line))].filter(\r\n (line) => line !== locs[0].line,\r\n );\r\n return lines.length ? { alsoAtLines: lines } : {};\r\n })(),\r\n }\r\n : {}),\r\n ...(group.some((w) => w.locsTruncated) ? { locsTruncated: true } : {}),\r\n };\r\n });\r\n}\r\n\r\n/** Keep only the warnings for these clients. An empty or absent list keeps all. */\r\nexport function forClients(warnings: CSSWarning[], clients?: string[]): CSSWarning[] {\r\n if (!clients?.length) return warnings;\r\n const wanted = new Set(clients);\r\n return warnings.filter((w) => wanted.has(w.client));\r\n}\r\n\r\n/** Every client id the engine knows, for validating a caller's filter. */\r\nexport function knownClientIds(): string[] {\r\n return EMAIL_CLIENTS.map((c) => c.id);\r\n}\r\n","import { compile } from \"@emailens/engine/compile\";\n\n/**\n * Getting a template to HTML before anything looks at it.\n *\n * The `format` parameter used to do nothing but choose the syntax of the fix\n * snippets, while the source was handed to the analyzer as-is. That is a quiet\n * and expensive lie: cheerio parses `<mjml><mj-section>…` without complaint,\n * finds no CSS in it, and the tools answer **zero findings**. An assistant\n * reads that as a clean bill of health and says the campaign is fine. A wrong\n * answer delivered confidently is worse than an error.\n *\n * So a non-HTML format is compiled first, with the same compilers the CLI uses.\n * They are optional peer dependencies of the engine (MJML alone is 56MB, and\n * this server is usually run through `npx`), so a caller can land on a machine\n * that has none of them. That case gets the engine's own \"install this\" message\n * plus the way out that needs nothing installed: compile it yourself and send\n * the HTML.\n */\n\nexport type SourceResult = { ok: true; html: string } | { ok: false; message: string };\n\nconst COMPILED: ReadonlySet<string> = new Set([\"jsx\", \"mjml\", \"maizzle\"]);\n\n/**\n * Is this \"the package isn't here\" rather than \"the template is wrong\"?\n *\n * The engine says `MJML compilation requires \"mjml\"` for a missing compiler\n * and `Sandbox strategy \"isolated-vm\" requires the \"isolated-vm\" package` when\n * the compiler is present but its sandbox is not, which is what React Email\n * hits first. Both are the same problem for the caller, so one pattern covers\n * them. Getting this wrong is cheap in one direction only: a misclassified\n * template error gains an irrelevant sentence, while a misclassified missing\n * package would tell someone to debug markup that is fine.\n */\nfunction isMissingCompiler(message: string): boolean {\n return /requires (?:the )?\"/.test(message);\n}\n\n/**\n * Compile `source` when `format` names a template language, or pass it through\n * when it is already HTML.\n */\nfunction isVueSfc(source: string): boolean {\n return /<template[\\s>]/i.test(source) && /<\\/template>/i.test(source);\n}\n\n/** An omitted format on a Vue SFC is maizzle. An explicit format is kept. */\nexport function resolveSourceFormat(source: string, format?: string): string {\n if (format) return format;\n return isVueSfc(source) ? \"maizzle\" : \"html\";\n}\n\nexport async function toHtml(source: string, format?: string): Promise<SourceResult> {\n const resolved = resolveSourceFormat(source, format);\n if (resolved === \"html\") return { ok: true, html: source };\n if (!COMPILED.has(resolved)) {\n return { ok: false, message: `Unknown format \"${resolved}\". Use html, jsx, mjml, or maizzle.` };\n }\n\n try {\n return { ok: true, html: await compile(source, resolved as \"jsx\" | \"mjml\" | \"maizzle\") };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (isMissingCompiler(message)) {\n return {\n ok: false,\n message:\n `${message}\\n\\nThat has to happen where this MCP server runs, which may not be somewhere ` +\n `you can install to. If it is not, compile the template yourself and send the resulting ` +\n `HTML with format \"html\".`,\n };\n }\n return { ok: false, message };\n }\n}\n","export const config = {\r\n get apiKey(): string | null {\r\n return process.env.EMAILENS_API_KEY ?? null;\r\n },\r\n get apiUrl(): string {\r\n // The app host, not the apex: emailens.dev redirects /api/* here, and a\r\n // redirect to another host drops the Authorization header on the way.\r\n return process.env.EMAILENS_API_URL ?? \"https://app.emailens.dev\";\r\n },\r\n get isHosted(): boolean {\r\n return this.apiKey !== null;\r\n },\r\n};\r\n","/**\r\n * The first version of each email audited in this session: the design its\r\n * later revisions are held to. An agent fixing findings rarely passes its own\r\n * draft back, so the server remembers it.\r\n *\r\n * Matched by <title> and then by text, because an agent rewrites markup but\r\n * keeps the words: a revision shares nearly all its text with the draft,\r\n * another email with the same title (often just the product name) does not.\r\n * A revision that rewrites the copy becomes a new draft: a missed comparison,\r\n * never a false one.\r\n * ponytail: in-memory, per process, capped; lost on restart, which only means\r\n * the next audit becomes the draft.\r\n */\r\n\r\nconst MAX_DRAFTS = 200;\r\n// Jaccard on words. Measured on the benchmark (359 emails, one title for all):\r\n// two different emails of one product reach 0.48, a fix revision keeps ~all.\r\nconst SAME_EMAIL = 0.7;\r\nconst drafts = new Map<string, { words: Set<string>; html: string }[]>();\r\n\r\nfunction title(html: string): string {\r\n return (html.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i)?.[1] ?? \"\").trim().toLowerCase();\r\n}\r\n\r\nfunction words(html: string): Set<string> {\r\n const text = html\r\n .replace(/<!--[\\s\\S]*?-->/g, \" \")\r\n .replace(/<(style|script|head)[\\s\\S]*?<\\/\\1>/gi, \" \")\r\n .replace(/<[^>]+>/g, \" \")\r\n .toLowerCase();\r\n return new Set(text.match(/[\\p{L}\\p{N}]{3,}/gu) ?? []);\r\n}\r\n\r\nfunction overlap(a: Set<string>, b: Set<string>): number {\r\n if (!a.size || !b.size) return 0;\r\n let shared = 0;\r\n for (const w of a) if (b.has(w)) shared++;\r\n return shared / (a.size + b.size - shared);\r\n}\r\n\r\n/** The draft this html revises, or this html itself when it is a new email. */\r\nexport function firstDraft(html: string): string {\r\n const key = title(html);\r\n const seen = words(html);\r\n const list = drafts.get(key) ?? [];\r\n const best = list\r\n .map((d) => ({ d, score: overlap(d.words, seen) }))\r\n .sort((a, b) => b.score - a.score)[0];\r\n if (best && best.score >= SAME_EMAIL) return best.d.html;\r\n if ([...drafts.values()].reduce((n, l) => n + l.length, 0) >= MAX_DRAFTS) drafts.clear();\r\n drafts.set(key, [...list, { words: seen, html }]);\r\n return html;\r\n}\r\n","import { readFile, realpath, stat } from \"node:fs/promises\";\nimport { extname } from \"node:path\";\nimport { MAX_HTML_SIZE } from \"@emailens/engine\";\n\n/**\n * The email a tool works on, passed inline or as a file path.\n *\n * Inline, a 60 KB email travels through the model's context on every call of a\n * lint, fix, re-check loop, and fix_email sends it back once more. A path keeps\n * it on disk: this server runs locally over stdio, beside the files the agent\n * is editing, so it can read them itself.\n *\n * The path is chosen by a model, which a prompt injection can steer, and\n * fix_email echoes its input while capture_screenshots and share_preview upload\n * it. So only email sources are read: the extension is checked on the resolved\n * file, after symlinks, and the size before reading.\n */\n\n// The format a file's extension implies. `.html` implies none: it may be a\n// Maizzle template, which only an explicit format can say.\nconst EXTENSIONS: Record<string, string | undefined> = {\n \".html\": undefined,\n \".htm\": undefined,\n \".mjml\": \"mjml\",\n \".jsx\": \"jsx\",\n \".tsx\": \"jsx\",\n \".vue\": \"maizzle\",\n};\n\nexport type Input =\n | { ok: true; html: string; format?: string; path?: string }\n | { ok: false; message: string };\n\nexport async function readSource(\n html: string | undefined,\n path: string | undefined,\n format?: string,\n names = { html: \"html\", path: \"path\" },\n): Promise<Input> {\n if ((html === undefined) === (path === undefined)) {\n return { ok: false, message: `Pass exactly one of \\`${names.html}\\` (the source) or \\`${names.path}\\` (a file holding it).` };\n }\n if (html !== undefined) return { ok: true, html, format };\n\n let real: string;\n try {\n real = await realpath(path!);\n } catch {\n return { ok: false, message: `No file at ${path}. Pass an absolute path, or the source itself as \\`${names.html}\\`.` };\n }\n const ext = extname(real).toLowerCase();\n if (!(ext in EXTENSIONS)) {\n return { ok: false, message: `${path} is not an email source. Accepted: ${Object.keys(EXTENSIONS).join(\", \")}.` };\n }\n const info = await stat(real);\n if (!info.isFile()) return { ok: false, message: `${path} is not a file.` };\n if (info.size > MAX_HTML_SIZE) {\n return { ok: false, message: `${path} exceeds the ${Math.round(MAX_HTML_SIZE / 1024)}KB limit.` };\n }\n return { ok: true, html: await readFile(real, \"utf8\"), format: format ?? EXTENSIONS[ext], path: real };\n}\n\n/**\n * fix_email's prompt without the source, when the caller has it on disk.\n *\n * The engine embeds the whole source as a fenced block; on a path that is the\n * largest part of the response and something the agent can read itself.\n */\nexport function withoutSource(prompt: string, html: string, format: string, path: string): string {\n return prompt.replace(`\\`\\`\\`${format}\\n${html}\\n\\`\\`\\``, () => `The source is in \\`${path}\\`. Read and edit it there.`);\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 app.emailens.dev/dashboard/api?utm_source=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?utm_source=mcp\"\n : \"Access denied. Check your plan at emailens.dev/pricing?utm_source=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?utm_source=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?utm_source=mcp\",\n \"2. Create a key at app.emailens.dev/dashboard/api?utm_source=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","MAX_HTML_SIZE","data","MAX_HTML_SIZE","EMAIL_CLIENTS","title"]}
|
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.
|
|
4
|
+
"version": "0.13.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.16.
|
|
31
|
+
"@emailens/engine": "^0.16.7",
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
33
33
|
"zod": "^3.25.76"
|
|
34
34
|
},
|