@emailens/mcp 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,183 +1,179 @@
1
- # @emailens/mcp
2
-
3
- MCP (Model Context Protocol) server for email compatibility analysis. Enables Claude and other MCP-compatible AI assistants to preview, analyze, and score HTML emails across 12 email clients.
4
-
5
- Built on top of [`@emailens/engine`](https://github.com/emailens/engine).
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install -g @emailens/mcp
11
- # or
12
- bunx @emailens/mcp
13
- ```
14
-
15
- ## Usage with Claude Desktop
16
-
17
- Add to your Claude Desktop config (`claude_desktop_config.json`):
18
-
19
- ```json
20
- {
21
- "mcpServers": {
22
- "emailens": {
23
- "command": "npx",
24
- "args": ["-y", "@emailens/mcp"]
25
- }
26
- }
27
- }
28
- ```
29
-
30
- Or if using Bun:
31
-
32
- ```json
33
- {
34
- "mcpServers": {
35
- "emailens": {
36
- "command": "bunx",
37
- "args": ["@emailens/mcp"]
38
- }
39
- }
40
- }
41
- ```
42
-
43
- ## Usage with Claude Code
44
-
45
- Add to your Claude Code settings:
46
-
47
- ```bash
48
- claude mcp add emailens -- npx -y @emailens/mcp
49
- ```
50
-
51
- ## Tools
52
-
53
- The server exposes four tools:
54
-
55
- ### `preview_email`
56
-
57
- Full email compatibility preview — transforms HTML per client, analyzes CSS, generates scores, and simulates dark mode.
58
-
59
- **Parameters:**
60
-
61
- | Name | Type | Required | Description |
62
- |---|---|---|---|
63
- | `html` | string | Yes | The email HTML source code |
64
- | `clients` | string[] | No | Filter to specific client IDs (e.g. `["gmail_web", "outlook_windows"]`). Omit for all 12 clients. |
65
- | `format` | enum | No | Input format: `"html"` (default), `"jsx"` (React Email), `"mjml"`, or `"maizzle"`. Controls which framework-specific fix snippets appear. |
66
-
67
- **Returns:** JSON with `overallScore` (0–100), `compatibilityScores` (per-client), `cssWarnings` (with fix snippets), `clientCount`, and `darkModeWarnings`.
68
-
69
- **Example prompt:**
70
- > Analyze this email HTML and tell me what will break in Gmail and Outlook:
71
- > ```html
72
- > <div style="display: flex; gap: 16px; border-radius: 8px;">...</div>
73
- > ```
74
-
75
- ### `analyze_email`
76
-
77
- Quick CSS compatibility analysis — returns warnings and per-client scores without full transforms. Faster than `preview_email` when you only need the compatibility report.
78
-
79
- **Parameters:**
80
-
81
- | Name | Type | Required | Description |
82
- |---|---|---|---|
83
- | `html` | string | Yes | The email HTML source code |
84
- | `format` | enum | No | Input format: `"html"`, `"jsx"`, `"mjml"`, or `"maizzle"` |
85
-
86
- **Returns:** JSON with `overallScore`, `scores` (per-client), `warningCount`, and `warnings` (with severity, message, and fix).
87
-
88
- **Example prompt:**
89
- > What's the compatibility score for this React Email template?
90
-
91
- ### `fix_email`
92
-
93
- Generate a structured fix prompt for email compatibility issues. Analyzes the HTML, classifies each warning as CSS-only or structural (requires HTML restructuring), estimates token usage, and returns a markdown prompt the AI assistant can apply directly. Use after `preview_email` or `analyze_email` to fix the issues found.
94
-
95
- **Parameters:**
96
-
97
- | Name | Type | Required | Description |
98
- |---|---|---|---|
99
- | `html` | string | Yes | The email HTML source code to fix |
100
- | `format` | enum | No | Input format: `"html"` (default), `"jsx"`, `"mjml"`, or `"maizzle"`. Controls the fix syntax. |
101
- | `scope` | enum | No | `"all"` (default) or `"current"` (requires `selectedClientId`) |
102
- | `selectedClientId` | string | No | Client ID to scope fixes to (e.g. `"outlook-windows"`). Only used when scope is `"current"`. |
103
-
104
- **Returns:** JSON with `totalWarnings`, `structuralWarnings`, `cssWarnings`, `tokenEstimate`, and the full fix prompt as markdown.
105
-
106
- **Example prompt:**
107
- > Fix this email HTML so it works in Outlook Windows — it uses flexbox and word-break which aren't supported.
108
-
109
- ### `list_clients`
110
-
111
- Lists all 12 supported email client IDs, display names, categories, rendering engines, and dark mode support. Useful for discovering valid client IDs to pass to `preview_email` or `fix_email`.
112
-
113
- **Parameters:** None
114
-
115
- **Returns:** JSON array of `{ id, name, category, engine, darkModeSupport }` objects.
116
-
117
- **Example prompt:**
118
- > What email clients does Emailens support?
119
-
120
- ## Supported Email Clients
121
-
122
- | Client | ID | Dark Mode |
123
- |---|---|---|
124
- | Gmail | `gmail-web` | Yes |
125
- | Gmail Android | `gmail-android` | Yes |
126
- | Gmail iOS | `gmail-ios` | Yes |
127
- | Outlook 365 | `outlook-web` | Yes |
128
- | Outlook Windows | `outlook-windows` | No |
129
- | Apple Mail | `apple-mail-macos` | Yes |
130
- | Apple Mail iOS | `apple-mail-ios` | Yes |
131
- | Yahoo Mail | `yahoo-mail` | Yes |
132
- | Samsung Mail | `samsung-mail` | Yes |
133
- | Thunderbird | `thunderbird` | No |
134
- | HEY Mail | `hey-mail` | Yes |
135
- | Superhuman | `superhuman` | Yes |
136
-
137
- ## Framework-Aware Fixes
138
-
139
- When you specify a `format`, the fix snippets in warnings are tailored to your framework:
140
-
141
- - **`jsx`** — References React Email components (`Row`, `Column`, `Font`, `Container` from `@react-email/components`)
142
- - **`mjml`** — References MJML elements (`mj-section`, `mj-column`, `mj-font`, `mj-style`)
143
- - **`maizzle`** — References Tailwind CSS classes and Maizzle config (`googleFonts`, MSO conditionals)
144
- - **`html`** (default) — Generic HTML with VML fallbacks for Outlook
145
-
146
- ## Fix Types (v0.2.0)
147
-
148
- Every warning now includes a `fixType` field:
149
-
150
- - **`css`** — Can be fixed with CSS swaps or fallbacks
151
- - **`structural`** — Requires HTML restructuring (tables, VML, MSO conditionals). CSS-only changes will NOT work.
152
-
153
- The `fix_email` tool uses this classification to generate targeted fix instructions that the AI assistant can apply directly.
154
-
155
- ## How It Works
156
-
157
- The MCP server is a thin wrapper around `@emailens/engine`. For each tool call, it:
158
-
159
- 1. Validates inputs with Zod schemas
160
- 2. Calls engine functions (`transformForAllClients`, `analyzeEmail`, `generateCompatibilityScore`, `simulateDarkMode`, `generateFixPrompt`, `estimateAiFixTokens`)
161
- 3. Formats and returns JSON results via stdio transport
162
-
163
- The engine uses [Cheerio](https://cheerio.js.org/) for HTML manipulation and [css-tree](https://github.com/csstree/csstree) for CSS parsing. No external API calls — all analysis and fix prompt generation runs locally. The `fix_email` tool generates a prompt for the AI assistant to apply — it does not call any AI API itself.
164
-
165
- ## Development
166
-
167
- ```bash
168
- # Install dependencies
169
- bun install
170
-
171
- # Build
172
- bun run build
173
-
174
- # Run locally
175
- bun run src/index.ts
176
-
177
- # Type check
178
- bun run typecheck
179
- ```
180
-
181
- ## License
182
-
183
- MIT
1
+ # @emailens/mcp
2
+
3
+ MCP server for email compatibility analysis. Analyze, preview, diff, and fix HTML emails across 15 email clients — plus capture real screenshots and create shareable links with an optional API key.
4
+
5
+ Built on [`@emailens/engine`](https://github.com/emailens/engine).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npx -y @emailens/mcp
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ ### Claude Desktop
16
+
17
+ Add to `claude_desktop_config.json`:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "emailens": {
23
+ "command": "npx",
24
+ "args": ["-y", "@emailens/mcp"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### Claude Code
31
+
32
+ ```bash
33
+ claude mcp add emailens -- npx -y @emailens/mcp
34
+ ```
35
+
36
+ ### With API Key (optional — unlocks screenshots + sharing)
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "emailens": {
42
+ "command": "npx",
43
+ "args": ["-y", "@emailens/mcp"],
44
+ "env": {
45
+ "EMAILENS_API_KEY": "ek_live_..."
46
+ }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ Get your free API key at [emailens.dev/settings/api-keys](https://emailens.dev/settings/api-keys).
53
+
54
+ ## Tools
55
+
56
+ ### Local Tools (no account needed)
57
+
58
+ #### `preview_email`
59
+
60
+ Full email compatibility preview — transforms HTML for 15 clients, analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.
61
+
62
+ | Parameter | Type | Required | Description |
63
+ |---|---|---|---|
64
+ | `html` | string | Yes | Email HTML source |
65
+ | `clients` | string[] | No | Filter to specific client IDs |
66
+ | `format` | enum | No | `"html"`, `"jsx"`, `"mjml"`, `"maizzle"` |
67
+
68
+ #### `analyze_email`
69
+
70
+ Quick CSS compatibility analysis — returns per-client scores and warnings. Faster than `audit_email` when you only need CSS compatibility.
71
+
72
+ | Parameter | Type | Required | Description |
73
+ |---|---|---|---|
74
+ | `html` | string | Yes | Email HTML source |
75
+ | `format` | enum | No | Input format |
76
+
77
+ #### `audit_email`
78
+
79
+ Comprehensive quality audit — CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), and template variables.
80
+
81
+ | Parameter | Type | Required | Description |
82
+ |---|---|---|---|
83
+ | `html` | string | Yes | Email HTML source |
84
+ | `format` | enum | No | Input format |
85
+ | `skip` | string[] | No | Checks to skip (e.g. `["spam", "images"]`) |
86
+
87
+ #### `fix_email`
88
+
89
+ Generate a structured fix prompt for compatibility issues. Returns markdown with fix instructions that the AI can apply directly.
90
+
91
+ | Parameter | Type | Required | Description |
92
+ |---|---|---|---|
93
+ | `html` | string | Yes | Email HTML to fix |
94
+ | `format` | enum | No | Controls fix syntax |
95
+ | `scope` | enum | No | `"all"` or `"current"` |
96
+ | `selectedClientId` | string | No | Client ID for scoped fixes |
97
+
98
+ #### `list_clients`
99
+
100
+ List all 15 supported email clients with IDs, names, engines, and dark mode support.
101
+
102
+ #### `diff_emails`
103
+
104
+ Compare two email HTML versions — shows score changes, fixed issues, and introduced issues per client.
105
+
106
+ | Parameter | Type | Required | Description |
107
+ |---|---|---|---|
108
+ | `before` | string | Yes | Original email HTML |
109
+ | `after` | string | Yes | Modified email HTML |
110
+ | `format` | enum | No | Input format |
111
+
112
+ #### `check_deliverability`
113
+
114
+ Check email deliverability for a domain — SPF, DKIM, DMARC, MX, BIMI records with a score and actionable issues.
115
+
116
+ | Parameter | Type | Required | Description |
117
+ |---|---|---|---|
118
+ | `domain` | string | Yes | Domain to check (e.g. `"company.com"`) |
119
+
120
+ ### Hosted Tools (require `EMAILENS_API_KEY`)
121
+
122
+ #### `capture_screenshots`
123
+
124
+ Capture real email screenshots across 15 clients in real browsers. Screenshots are hosted on CDN.
125
+
126
+ | Parameter | Type | Required | Description |
127
+ |---|---|---|---|
128
+ | `html` | string | Yes | Email HTML source |
129
+ | `format` | enum | No | Input format |
130
+ | `clients` | string[] | No | Filter clients |
131
+ | `modes` | string[] | No | `["light"]`, `["dark"]`, or `["light", "dark"]` |
132
+ | `title` | string | No | Name for the preview |
133
+
134
+ Free plan: 30 previews/day. [Sign up](https://emailens.dev?ref=mcp)
135
+
136
+ #### `share_preview`
137
+
138
+ Create a shareable link. Recipients see the full analysis without an account.
139
+
140
+ | Parameter | Type | Required | Description |
141
+ |---|---|---|---|
142
+ | `html` | string | Yes | Email HTML source |
143
+ | `title` | string | No | Display title |
144
+ | `format` | enum | No | Input format |
145
+
146
+ Requires Dev plan ($9/mo). Share links expire after 7 days (Dev) or never (Pro).
147
+
148
+ ## Supported Email Clients (15)
149
+
150
+ | Client | ID | Dark Mode | Notes |
151
+ |---|---|---|---|
152
+ | Gmail | `gmail-web` | Yes | |
153
+ | Gmail Android | `gmail-android` | Yes | |
154
+ | Gmail iOS | `gmail-ios` | Yes | |
155
+ | Outlook 365 | `outlook-web` | Yes | |
156
+ | Outlook Windows | `outlook-windows` | No | |
157
+ | Outlook Windows Legacy | `outlook-windows-legacy` | No | Deprecated Oct 2026 |
158
+ | Outlook iOS | `outlook-ios` | Yes | New in v0.4.0 |
159
+ | Outlook Android | `outlook-android` | Yes | New in v0.4.0 |
160
+ | Apple Mail | `apple-mail-macos` | Yes | |
161
+ | Apple Mail iOS | `apple-mail-ios` | Yes | |
162
+ | Yahoo Mail | `yahoo-mail` | Yes | |
163
+ | Samsung Mail | `samsung-mail` | Yes | |
164
+ | Thunderbird | `thunderbird` | No | |
165
+ | HEY Mail | `hey-mail` | Yes | |
166
+ | Superhuman | `superhuman` | Yes | |
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ bun install
172
+ bun run build
173
+ bun test
174
+ bun run typecheck
175
+ ```
176
+
177
+ ## License
178
+
179
+ MIT
package/dist/index.js CHANGED
@@ -1,47 +1,136 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
 
4
3
  // src/index.ts
5
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
6
  import { z } from "zod";
8
7
  import {
9
- transformForAllClients,
8
+ createSession,
10
9
  analyzeEmail,
11
10
  generateCompatibilityScore,
12
- simulateDarkMode,
13
11
  generateFixPrompt,
14
12
  estimateAiFixTokens,
15
- auditEmail,
16
13
  structuralWarnings,
17
- EMAIL_CLIENTS
14
+ diffResults,
15
+ toPlainText,
16
+ EMAIL_CLIENTS,
17
+ MAX_HTML_SIZE
18
18
  } from "@emailens/engine";
19
- var toPlainText = null;
20
- try {
21
- ({ toPlainText } = await import("@emailens/engine"));
22
- } catch {
19
+
20
+ // src/config.ts
21
+ var config = {
22
+ get apiKey() {
23
+ return process.env.EMAILENS_API_KEY ?? null;
24
+ },
25
+ get apiUrl() {
26
+ return process.env.EMAILENS_API_URL ?? "https://emailens.dev";
27
+ },
28
+ get isHosted() {
29
+ return this.apiKey !== null;
30
+ }
31
+ };
32
+
33
+ // src/hosted.ts
34
+ async function apiRequest(path, options) {
35
+ const { apiKey, apiUrl, method = "GET", body, timeoutMs = 3e4 } = options;
36
+ const url = `${apiUrl}${path}`;
37
+ const controller = new AbortController();
38
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
39
+ try {
40
+ const response = await fetch(url, {
41
+ method: method !== "GET" ? method : body ? "POST" : "GET",
42
+ headers: {
43
+ Authorization: `Bearer ${apiKey}`,
44
+ "Content-Type": "application/json"
45
+ },
46
+ body: body ? JSON.stringify(body) : void 0,
47
+ signal: controller.signal
48
+ });
49
+ if (response.status === 401) {
50
+ return {
51
+ isError: true,
52
+ error: "Invalid or revoked API key. Generate a new one at emailens.dev/settings/api-keys?ref=mcp"
53
+ };
54
+ }
55
+ if (response.status === 403) {
56
+ const data2 = await response.json().catch(() => ({}));
57
+ return {
58
+ isError: true,
59
+ error: data2.upgrade ? "This feature requires a plan upgrade. Visit emailens.dev/pricing?ref=mcp" : "Access denied. Check your plan at emailens.dev/pricing?ref=mcp"
60
+ };
61
+ }
62
+ if (response.status === 429) {
63
+ const data2 = await response.json().catch(() => ({}));
64
+ return {
65
+ isError: true,
66
+ error: `Daily quota exceeded${data2.limit ? ` (${data2.limit - (data2.remaining ?? 0)}/${data2.limit})` : ""}. Resets tomorrow. Upgrade at emailens.dev/pricing?ref=mcp`
67
+ };
68
+ }
69
+ if (!response.ok) {
70
+ const data2 = await response.json().catch(() => ({}));
71
+ return { isError: true, error: data2.error ?? `API error: ${response.status}` };
72
+ }
73
+ const data = await response.json();
74
+ return { data };
75
+ } catch (err) {
76
+ if (err.name === "AbortError") {
77
+ return { isError: true, error: `Request timed out after ${timeoutMs / 1e3}s. Try again or check emailens.dev status.` };
78
+ }
79
+ return { isError: true, error: `Network error: ${err.message}` };
80
+ } finally {
81
+ clearTimeout(timer);
82
+ }
83
+ }
84
+ function mcpError(message) {
85
+ return {
86
+ content: [{ type: "text", text: message }],
87
+ isError: true
88
+ };
23
89
  }
90
+ function noApiKeyError(toolName, extraInfo) {
91
+ const lines = [
92
+ `${toolName} requires an Emailens API key.`,
93
+ "",
94
+ "1. Sign up free at emailens.dev?ref=mcp",
95
+ "2. Create a key at emailens.dev/settings/api-keys?ref=mcp",
96
+ "3. Add to your MCP config:",
97
+ "",
98
+ ' "env": { "EMAILENS_API_KEY": "ek_live_..." }'
99
+ ];
100
+ if (extraInfo) {
101
+ lines.push("", extraInfo);
102
+ } else {
103
+ lines.push("", "Free plan includes 30 previews/day (each preview can include screenshots).");
104
+ }
105
+ return mcpError(lines.join("\n"));
106
+ }
107
+
108
+ // src/index.ts
109
+ var _checkDeliverability = null;
24
110
  function toFramework(format) {
25
111
  if (format === "jsx" || format === "mjml" || format === "maizzle") return format;
26
112
  return void 0;
27
113
  }
114
+ var formatEnum = z.enum(["html", "jsx", "mjml", "maizzle"]).optional();
115
+ function validateHtmlSize(html) {
116
+ if (html.length > MAX_HTML_SIZE) {
117
+ return mcpError(`HTML input exceeds ${Math.round(MAX_HTML_SIZE / 1024)}KB limit. Reduce the email size and try again.`);
118
+ }
119
+ return null;
120
+ }
28
121
  var server = new McpServer({
29
122
  name: "emailens",
30
- version: "0.3.0"
123
+ version: "0.4.0"
31
124
  });
32
125
  server.registerTool(
33
126
  "preview_email",
34
127
  {
35
128
  title: "Preview Email",
36
- description: "Analyze an HTML email and see how it renders across 12 email clients (Gmail, Outlook, Apple Mail, Yahoo, Samsung, Thunderbird, HEY, Superhuman). Returns CSS transforms, compatibility warnings with fix snippets, dark mode simulation, and per-client scores.",
129
+ description: "Full email compatibility preview \u2014 transforms HTML for 15 email clients (Gmail, Outlook, Apple Mail, Yahoo, Samsung, Thunderbird, HEY, Superhuman), analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.",
37
130
  inputSchema: {
38
131
  html: z.string().describe("The email HTML source code"),
39
- clients: z.array(z.string()).optional().describe(
40
- "Optional array of client IDs to filter (e.g. ['gmail-web', 'outlook-windows']). Omit for all clients."
41
- ),
42
- format: z.enum(["html", "jsx", "mjml", "maizzle"]).optional().describe(
43
- "Input format of the email source: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Controls which framework-specific fix snippets appear in the warnings."
44
- )
132
+ clients: z.array(z.string()).optional().describe("Optional client ID filter (e.g. ['gmail-web', 'outlook-windows'])"),
133
+ format: formatEnum.describe("Input format: 'html' (default), 'jsx', 'mjml', or 'maizzle'")
45
134
  },
46
135
  annotations: {
47
136
  title: "Preview Email",
@@ -52,46 +141,88 @@ server.registerTool(
52
141
  }
53
142
  },
54
143
  async ({ html, clients, format }) => {
144
+ const sizeError = validateHtmlSize(html);
145
+ if (sizeError) return sizeError;
55
146
  const validClientIds = new Set(EMAIL_CLIENTS.map((c) => c.id));
56
- let transforms = transformForAllClients(html);
147
+ const framework = toFramework(format);
148
+ const session = createSession(html, { framework });
149
+ let transforms;
57
150
  if (clients) {
58
151
  const filter = clients.filter((c) => validClientIds.has(c));
59
152
  if (filter.length === 0) {
60
- return {
61
- content: [
62
- {
63
- type: "text",
64
- text: JSON.stringify(
65
- {
66
- error: "No valid client IDs provided",
67
- validClientIds: Array.from(validClientIds)
68
- },
69
- null,
70
- 2
71
- )
72
- }
73
- ],
74
- isError: true
75
- };
153
+ return mcpError(
154
+ JSON.stringify({ error: "No valid client IDs provided", validClientIds: Array.from(validClientIds) }, null, 2)
155
+ );
76
156
  }
77
- transforms = transforms.filter(
78
- (t) => filter.includes(t.clientId)
79
- );
157
+ transforms = filter.map((c) => session.transformForClient(c));
158
+ } else {
159
+ transforms = session.transformForAllClients();
80
160
  }
81
- const warnings = analyzeEmail(html, toFramework(format));
82
- const scores = generateCompatibilityScore(warnings);
161
+ const warnings = session.analyze();
162
+ const scores = session.score(warnings);
163
+ const inboxPreview = session.extractInboxPreview();
164
+ const sizeReport = session.checkSize();
83
165
  const darkMode = {};
84
166
  for (const t of transforms) {
85
- darkMode[t.clientId] = simulateDarkMode(t.html, t.clientId);
167
+ darkMode[t.clientId] = session.simulateDarkMode(t.clientId);
168
+ }
169
+ const scoreValues = Object.values(scores);
170
+ const overallScore = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
171
+ const plainText = toPlainText(html);
172
+ const result = {
173
+ overallScore,
174
+ compatibilityScores: scores,
175
+ cssWarnings: warnings.map((w) => ({
176
+ client: w.client,
177
+ property: w.property,
178
+ severity: w.severity,
179
+ message: w.message,
180
+ suggestion: w.suggestion,
181
+ fix: w.fix,
182
+ fixType: w.fixType
183
+ })),
184
+ inboxPreview,
185
+ sizeReport,
186
+ ...plainText ? { plainText } : {},
187
+ clientCount: transforms.length,
188
+ darkModeWarnings: Object.entries(darkMode).reduce(
189
+ (acc, [clientId, dm]) => {
190
+ if (dm.warnings.length > 0) acc[clientId] = dm.warnings.length;
191
+ return acc;
192
+ },
193
+ {}
194
+ )
195
+ };
196
+ if (!config.isHosted && overallScore < 90) {
197
+ result.tip = "Capture real screenshots to verify these issues across clients. Add EMAILENS_API_KEY to your MCP config \u2192 emailens.dev/settings/api-keys?ref=mcp";
86
198
  }
199
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
200
+ }
201
+ );
202
+ server.registerTool(
203
+ "analyze_email",
204
+ {
205
+ title: "Analyze Email",
206
+ description: "Quick CSS compatibility analysis \u2014 returns warnings and per-client scores. Use audit_email for full quality report (spam, links, a11y, images, etc.).",
207
+ inputSchema: {
208
+ html: z.string().describe("The email HTML source code"),
209
+ format: formatEnum.describe("Input format for framework-specific fix snippets")
210
+ },
211
+ annotations: {
212
+ title: "Analyze Email",
213
+ readOnlyHint: true,
214
+ destructiveHint: false,
215
+ idempotentHint: true,
216
+ openWorldHint: false
217
+ }
218
+ },
219
+ async ({ html, format }) => {
220
+ const sizeError = validateHtmlSize(html);
221
+ if (sizeError) return sizeError;
222
+ const warnings = analyzeEmail(html, toFramework(format));
223
+ const scores = generateCompatibilityScore(warnings);
87
224
  const scoreValues = Object.values(scores);
88
- const overallScore = scoreValues.length > 0 ? Math.round(
89
- scoreValues.reduce(
90
- (a, b) => a + b.score,
91
- 0
92
- ) / scoreValues.length
93
- ) : 0;
94
- const plainText = toPlainText ? toPlainText(html) : void 0;
225
+ const overallScore = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
95
226
  return {
96
227
  content: [
97
228
  {
@@ -99,8 +230,9 @@ server.registerTool(
99
230
  text: JSON.stringify(
100
231
  {
101
232
  overallScore,
102
- compatibilityScores: scores,
103
- cssWarnings: warnings.map((w) => ({
233
+ scores,
234
+ warningCount: warnings.length,
235
+ warnings: warnings.map((w) => ({
104
236
  client: w.client,
105
237
  property: w.property,
106
238
  severity: w.severity,
@@ -108,18 +240,7 @@ server.registerTool(
108
240
  suggestion: w.suggestion,
109
241
  fix: w.fix,
110
242
  fixType: w.fixType
111
- })),
112
- ...plainText ? { plainText } : {},
113
- clientCount: transforms.length,
114
- darkModeWarnings: Object.entries(darkMode).reduce(
115
- (acc, [clientId, dm]) => {
116
- if (dm.warnings.length > 0) {
117
- acc[clientId] = dm.warnings.length;
118
- }
119
- return acc;
120
- },
121
- {}
122
- )
243
+ }))
123
244
  },
124
245
  null,
125
246
  2
@@ -133,15 +254,11 @@ server.registerTool(
133
254
  "audit_email",
134
255
  {
135
256
  title: "Audit Email",
136
- description: "Run ALL email checks in one call: CSS compatibility, spam scoring, link validation, accessibility audit, and image analysis. Returns a unified report. Use --skip to omit specific checks.",
257
+ description: "Comprehensive email quality audit \u2014 CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), and template variables. Use skip to omit specific checks.",
137
258
  inputSchema: {
138
259
  html: z.string().describe("The email HTML source code"),
139
- format: z.enum(["html", "jsx", "mjml", "maizzle"]).optional().describe(
140
- "Input format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'."
141
- ),
142
- skip: z.array(z.enum(["spam", "links", "accessibility", "images", "compatibility"])).optional().describe(
143
- "Array of checks to skip (e.g. ['spam', 'images'])."
144
- )
260
+ format: formatEnum.describe("Input format for framework-specific fix snippets"),
261
+ skip: z.array(z.enum(["spam", "links", "accessibility", "images", "compatibility", "inboxPreview", "size", "templateVariables"])).optional().describe("Checks to skip (e.g. ['spam', 'images'])")
145
262
  },
146
263
  annotations: {
147
264
  title: "Audit Email",
@@ -152,17 +269,12 @@ server.registerTool(
152
269
  }
153
270
  },
154
271
  async ({ html, format, skip }) => {
155
- const report = auditEmail(html, {
156
- framework: toFramework(format),
157
- skip
158
- });
272
+ const sizeError = validateHtmlSize(html);
273
+ if (sizeError) return sizeError;
274
+ const session = createSession(html, { framework: toFramework(format) });
275
+ const report = session.audit({ skip });
159
276
  const scoreValues = Object.values(report.compatibility.scores);
160
- const overallCompatibility = scoreValues.length > 0 ? Math.round(
161
- scoreValues.reduce(
162
- (a, b) => a + b.score,
163
- 0
164
- ) / scoreValues.length
165
- ) : 0;
277
+ const overallCompatibility = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
166
278
  return {
167
279
  content: [
168
280
  {
@@ -186,66 +298,10 @@ server.registerTool(
186
298
  spam: report.spam,
187
299
  links: report.links,
188
300
  accessibility: report.accessibility,
189
- images: report.images
190
- },
191
- null,
192
- 2
193
- )
194
- }
195
- ]
196
- };
197
- }
198
- );
199
- server.registerTool(
200
- "analyze_email",
201
- {
202
- title: "Analyze Email",
203
- description: "CSS compatibility analysis with quality reports \u2014 returns warnings, per-client scores, plus spam, link, accessibility, and image analysis. For compatibility-only results, use skip. For full transforms + dark mode, use preview_email.",
204
- inputSchema: {
205
- html: z.string().describe("The email HTML source code"),
206
- format: z.enum(["html", "jsx", "mjml", "maizzle"]).optional().describe(
207
- "Input format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Determines which framework-specific fix snippets are returned."
208
- )
209
- },
210
- annotations: {
211
- title: "Analyze Email",
212
- readOnlyHint: true,
213
- destructiveHint: false,
214
- idempotentHint: true,
215
- openWorldHint: false
216
- }
217
- },
218
- async ({ html, format }) => {
219
- const report = auditEmail(html, { framework: toFramework(format) });
220
- const scoreValues = Object.values(report.compatibility.scores);
221
- const overallScore = scoreValues.length > 0 ? Math.round(
222
- scoreValues.reduce(
223
- (a, b) => a + b.score,
224
- 0
225
- ) / scoreValues.length
226
- ) : 0;
227
- return {
228
- content: [
229
- {
230
- type: "text",
231
- text: JSON.stringify(
232
- {
233
- overallScore,
234
- scores: report.compatibility.scores,
235
- warningCount: report.compatibility.warnings.length,
236
- warnings: report.compatibility.warnings.map((w) => ({
237
- client: w.client,
238
- property: w.property,
239
- severity: w.severity,
240
- message: w.message,
241
- suggestion: w.suggestion,
242
- fix: w.fix,
243
- fixType: w.fixType
244
- })),
245
- spam: report.spam,
246
- links: report.links,
247
- accessibility: report.accessibility,
248
- images: report.images
301
+ images: report.images,
302
+ inboxPreview: report.inboxPreview,
303
+ size: report.size,
304
+ templateVariables: report.templateVariables
249
305
  },
250
306
  null,
251
307
  2
@@ -259,18 +315,12 @@ server.registerTool(
259
315
  "fix_email",
260
316
  {
261
317
  title: "Fix Email",
262
- description: "Generate a structured fix prompt for email compatibility issues. Returns a markdown prompt with the original code, all detected issues (with fix type: CSS or structural), fix snippets, and format-specific instructions. The AI assistant can then apply these fixes directly. Use after preview_email or analyze_email to fix the issues found.",
318
+ 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.",
263
319
  inputSchema: {
264
320
  html: z.string().describe("The email HTML source code to fix"),
265
- format: z.enum(["html", "jsx", "mjml", "maizzle"]).optional().describe(
266
- "Input format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Controls the fix syntax in the prompt."
267
- ),
268
- scope: z.enum(["all", "current"]).optional().describe(
269
- "Fix scope: 'all' (default) fixes for all clients, 'current' fixes for a single client (requires selectedClientId)."
270
- ),
271
- selectedClientId: z.string().optional().describe(
272
- "Client ID to scope fixes to (e.g. 'outlook-windows'). Only used when scope is 'current'."
273
- )
321
+ format: formatEnum.describe("Input format \u2014 controls fix syntax"),
322
+ scope: z.enum(["all", "current"]).optional().describe("'all' (default) or 'current' (requires selectedClientId)"),
323
+ selectedClientId: z.string().optional().describe("Client ID to scope fixes to (e.g. 'outlook-windows')")
274
324
  },
275
325
  annotations: {
276
326
  title: "Fix Email",
@@ -281,6 +331,8 @@ server.registerTool(
281
331
  }
282
332
  },
283
333
  async ({ html, format, scope, selectedClientId }) => {
334
+ const sizeError = validateHtmlSize(html);
335
+ if (sizeError) return sizeError;
284
336
  const framework = toFramework(format);
285
337
  const fixScope = scope === "current" ? "current" : "all";
286
338
  const inputFormat = format || "html";
@@ -291,11 +343,7 @@ server.registerTool(
291
343
  content: [
292
344
  {
293
345
  type: "text",
294
- text: JSON.stringify(
295
- { message: "No compatibility issues found \u2014 nothing to fix.", overallScore: 100 },
296
- null,
297
- 2
298
- )
346
+ text: JSON.stringify({ message: "No compatibility issues found \u2014 nothing to fix.", overallScore: 100 }, null, 2)
299
347
  }
300
348
  ]
301
349
  };
@@ -331,16 +379,13 @@ server.registerTool(
331
379
  estimatedOutputTokens: estimate.estimatedOutputTokens,
332
380
  truncated: estimate.truncated
333
381
  },
334
- note: "Apply the fixes in the prompt below to the email code. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes."
382
+ note: "Apply the fixes in the prompt below. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes."
335
383
  },
336
384
  null,
337
385
  2
338
386
  )
339
387
  },
340
- {
341
- type: "text",
342
- text: prompt
343
- }
388
+ { type: "text", text: prompt }
344
389
  ]
345
390
  };
346
391
  }
@@ -349,7 +394,7 @@ server.registerTool(
349
394
  "list_clients",
350
395
  {
351
396
  title: "List Email Clients",
352
- description: "List all 12 supported email client IDs, display names, categories, rendering engines, and dark mode support.",
397
+ description: "List all 15 supported email clients with IDs, names, engines, and dark mode support.",
353
398
  annotations: {
354
399
  title: "List Email Clients",
355
400
  readOnlyHint: true,
@@ -364,15 +409,256 @@ server.registerTool(
364
409
  {
365
410
  type: "text",
366
411
  text: JSON.stringify(
367
- EMAIL_CLIENTS.map(
368
- (c) => ({
369
- id: c.id,
370
- name: c.name,
371
- category: c.category,
372
- engine: c.engine,
373
- darkModeSupport: c.darkModeSupport
374
- })
375
- ),
412
+ EMAIL_CLIENTS.map((c) => ({
413
+ id: c.id,
414
+ name: c.name,
415
+ category: c.category,
416
+ engine: c.engine,
417
+ darkModeSupport: c.darkModeSupport,
418
+ ...c.deprecated ? { deprecated: c.deprecated } : {}
419
+ })),
420
+ null,
421
+ 2
422
+ )
423
+ }
424
+ ]
425
+ };
426
+ }
427
+ );
428
+ server.registerTool(
429
+ "diff_emails",
430
+ {
431
+ title: "Diff Emails",
432
+ description: "Compare two email HTML versions \u2014 shows score changes, fixed issues, and newly introduced issues per client. Use after making fixes to verify improvements.",
433
+ inputSchema: {
434
+ before: z.string().describe("Original email HTML"),
435
+ after: z.string().describe("Modified email HTML"),
436
+ format: formatEnum.describe("Input format for framework-specific analysis")
437
+ },
438
+ annotations: {
439
+ title: "Diff Emails",
440
+ readOnlyHint: true,
441
+ destructiveHint: false,
442
+ idempotentHint: true,
443
+ openWorldHint: false
444
+ }
445
+ },
446
+ async ({ before, after, format }) => {
447
+ const beforeSizeError = validateHtmlSize(before);
448
+ if (beforeSizeError) return beforeSizeError;
449
+ const afterSizeError = validateHtmlSize(after);
450
+ if (afterSizeError) return afterSizeError;
451
+ const framework = toFramework(format);
452
+ const beforeWarnings = analyzeEmail(before, framework);
453
+ const beforeScores = generateCompatibilityScore(beforeWarnings);
454
+ const afterWarnings = analyzeEmail(after, framework);
455
+ const afterScores = generateCompatibilityScore(afterWarnings);
456
+ const results = diffResults(
457
+ { scores: beforeScores, warnings: beforeWarnings },
458
+ { scores: afterScores, warnings: afterWarnings }
459
+ );
460
+ let clientsImproved = 0;
461
+ let clientsRegressed = 0;
462
+ let clientsUnchanged = 0;
463
+ let totalDelta = 0;
464
+ const mappedResults = results.map((r) => {
465
+ if (r.scoreDelta > 0) clientsImproved++;
466
+ else if (r.scoreDelta < 0) clientsRegressed++;
467
+ else clientsUnchanged++;
468
+ totalDelta += r.scoreDelta;
469
+ return {
470
+ clientId: r.clientId,
471
+ scoreBefore: r.scoreBefore,
472
+ scoreAfter: r.scoreAfter,
473
+ scoreDelta: r.scoreDelta,
474
+ fixed: r.fixed.map((w) => ({ property: w.property, client: w.client, message: w.message })),
475
+ introduced: r.introduced.map((w) => ({ property: w.property, client: w.client, message: w.message })),
476
+ unchanged: r.unchanged.length
477
+ };
478
+ });
479
+ return {
480
+ content: [
481
+ {
482
+ type: "text",
483
+ text: JSON.stringify(
484
+ {
485
+ results: mappedResults,
486
+ summary: {
487
+ clientsImproved,
488
+ clientsRegressed,
489
+ clientsUnchanged,
490
+ avgScoreDelta: results.length > 0 ? Math.round(totalDelta / results.length * 10) / 10 : 0
491
+ }
492
+ },
493
+ null,
494
+ 2
495
+ )
496
+ }
497
+ ]
498
+ };
499
+ }
500
+ );
501
+ server.registerTool(
502
+ "check_deliverability",
503
+ {
504
+ title: "Check Deliverability",
505
+ description: "Check email deliverability for a domain \u2014 SPF, DKIM, DMARC, MX, and BIMI records. Returns a score (0-100) and actionable issues. Uses DNS lookups (no API key needed).",
506
+ inputSchema: {
507
+ domain: z.string().min(3).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i, "Invalid domain format").describe("Domain to check (e.g. 'company.com')")
508
+ },
509
+ annotations: {
510
+ title: "Check Deliverability",
511
+ readOnlyHint: true,
512
+ destructiveHint: false,
513
+ idempotentHint: true,
514
+ openWorldHint: true
515
+ }
516
+ },
517
+ async ({ domain }) => {
518
+ try {
519
+ if (!_checkDeliverability) {
520
+ const mod = await import("@emailens/engine/server");
521
+ _checkDeliverability = mod.checkDeliverability;
522
+ }
523
+ const report = await _checkDeliverability(domain);
524
+ return {
525
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
526
+ };
527
+ } catch (err) {
528
+ const msg = err.message ?? String(err);
529
+ if (msg.includes("Cannot find") || msg.includes("cannot find") || msg.includes("MODULE_NOT_FOUND")) {
530
+ return mcpError(
531
+ "check_deliverability requires @emailens/engine >=0.9.1 with the /server subpath. Update with: npm install @emailens/engine@latest"
532
+ );
533
+ }
534
+ return mcpError(`Deliverability check failed for "${domain}": ${msg}`);
535
+ }
536
+ }
537
+ );
538
+ server.registerTool(
539
+ "capture_screenshots",
540
+ {
541
+ title: "Capture Screenshots",
542
+ description: "Capture real email screenshots across 15 clients (Gmail, Outlook, Apple Mail, etc.) with light and dark mode variants. Screenshots are rendered in real browsers and hosted on CDN. Requires EMAILENS_API_KEY env var \u2014 free plan at emailens.dev?ref=mcp.",
543
+ inputSchema: {
544
+ html: z.string().describe("The email HTML source code"),
545
+ format: formatEnum.describe("Input format"),
546
+ clients: z.array(z.string()).optional().describe("Filter to specific client IDs"),
547
+ modes: z.array(z.enum(["light", "dark"])).optional().describe("Screenshot modes (default: ['light'])"),
548
+ title: z.string().optional().describe("Name for the hosted preview")
549
+ },
550
+ annotations: {
551
+ title: "Capture Screenshots",
552
+ readOnlyHint: false,
553
+ destructiveHint: false,
554
+ idempotentHint: false,
555
+ openWorldHint: true
556
+ }
557
+ },
558
+ async ({ html, format, clients, modes, title }) => {
559
+ if (!config.isHosted) {
560
+ return noApiKeyError("capture_screenshots");
561
+ }
562
+ const opts = { apiKey: config.apiKey, apiUrl: config.apiUrl };
563
+ const previewResult = await apiRequest("/api/preview", {
564
+ ...opts,
565
+ body: { html, format, clients, title },
566
+ timeoutMs: 3e4
567
+ });
568
+ if (previewResult.isError) {
569
+ return mcpError(previewResult.error);
570
+ }
571
+ const previewId = previewResult.data.id;
572
+ let screenshotResult = await apiRequest(`/api/preview/${previewId}/screenshots`, {
573
+ ...opts,
574
+ body: { clients, modes },
575
+ timeoutMs: 12e4
576
+ });
577
+ if (screenshotResult.isError && screenshotResult.error?.includes("timed out")) {
578
+ screenshotResult = await apiRequest(`/api/preview/${previewId}/screenshots`, {
579
+ ...opts,
580
+ body: { clients, modes },
581
+ timeoutMs: 12e4
582
+ });
583
+ }
584
+ if (screenshotResult.isError) {
585
+ return {
586
+ content: [
587
+ {
588
+ type: "text",
589
+ text: JSON.stringify(
590
+ {
591
+ previewId,
592
+ previewUrl: `${config.apiUrl}/preview/${previewId}`,
593
+ overallScore: previewResult.data.overallScore,
594
+ screenshots: {},
595
+ screenshotStatus: "failed",
596
+ captured: [],
597
+ failed: [],
598
+ note: `Preview created but screenshot capture failed: ${screenshotResult.error}. View the full preview at the URL above.`
599
+ },
600
+ null,
601
+ 2
602
+ )
603
+ }
604
+ ]
605
+ };
606
+ }
607
+ const ss = screenshotResult.data;
608
+ const result = {
609
+ previewId,
610
+ previewUrl: `${config.apiUrl}/preview/${previewId}`,
611
+ overallScore: previewResult.data.overallScore,
612
+ screenshots: ss.screenshots,
613
+ screenshotStatus: ss.screenshotStatus,
614
+ captured: ss.captured,
615
+ failed: ss.failed
616
+ };
617
+ if (ss.failed?.length > 0) {
618
+ result.note = `${ss.failed.length} of ${(ss.captured?.length ?? 0) + ss.failed.length} screenshots failed: ${ss.failed.join(", ")}`;
619
+ }
620
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
621
+ }
622
+ );
623
+ server.registerTool(
624
+ "share_preview",
625
+ {
626
+ title: "Share Preview",
627
+ 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).",
628
+ inputSchema: {
629
+ html: z.string().describe("The email HTML source code"),
630
+ title: z.string().optional().describe("Display title for the share page"),
631
+ format: formatEnum.describe("Input format")
632
+ },
633
+ annotations: {
634
+ title: "Share Preview",
635
+ readOnlyHint: false,
636
+ destructiveHint: false,
637
+ idempotentHint: false,
638
+ openWorldHint: true
639
+ }
640
+ },
641
+ async ({ html, title, format }) => {
642
+ if (!config.isHosted) {
643
+ return noApiKeyError("share_preview", "Shareable links require the Dev plan ($9/mo).");
644
+ }
645
+ const result = await apiRequest("/api/share", {
646
+ apiKey: config.apiKey,
647
+ apiUrl: config.apiUrl,
648
+ body: { html, title, format }
649
+ });
650
+ if (result.isError) {
651
+ return mcpError(result.error);
652
+ }
653
+ return {
654
+ content: [
655
+ {
656
+ type: "text",
657
+ text: JSON.stringify(
658
+ {
659
+ shareUrl: `${config.apiUrl}/share/${result.data.id}`,
660
+ expiresAt: result.data.expiresAt
661
+ },
376
662
  null,
377
663
  2
378
664
  )
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\r\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\r\nimport { z } from \"zod\";\r\nimport {\r\n transformForAllClients,\r\n analyzeEmail,\r\n generateCompatibilityScore,\r\n simulateDarkMode,\r\n generateFixPrompt,\r\n estimateAiFixTokens,\r\n auditEmail,\r\n structuralWarnings,\r\n EMAIL_CLIENTS,\r\n type Framework,\r\n type CSSWarning,\r\n type AuditReport,\r\n} from \"@emailens/engine\";\r\n\r\n// toPlainText available in engine >=0.8.6\r\nlet toPlainText: ((html: string) => string) | null = null;\r\ntry {\r\n ({ toPlainText } = await import(\"@emailens/engine\"));\r\n} catch {\r\n // older engine version\r\n}\r\n\r\nfunction toFramework(format?: string): Framework | undefined {\r\n if (format === \"jsx\" || format === \"mjml\" || format === \"maizzle\") return format;\r\n return undefined;\r\n}\r\n\r\nconst server = new McpServer({\r\n name: \"emailens\",\r\n version: \"0.3.0\",\r\n});\r\n\r\n// ── Tool: preview_email ─────────────────────────────────────────\r\nserver.registerTool(\r\n \"preview_email\",\r\n {\r\n title: \"Preview Email\",\r\n description:\r\n \"Analyze an HTML email and see how it renders across 12 email clients (Gmail, Outlook, Apple Mail, Yahoo, Samsung, Thunderbird, HEY, Superhuman). Returns CSS transforms, compatibility warnings with fix snippets, dark mode simulation, and per-client scores.\",\r\n inputSchema: {\r\n html: z.string().describe(\"The email HTML source code\"),\r\n clients: z\r\n .array(z.string())\r\n .optional()\r\n .describe(\r\n \"Optional array of client IDs to filter (e.g. ['gmail-web', 'outlook-windows']). Omit for all clients.\"\r\n ),\r\n format: z\r\n .enum([\"html\", \"jsx\", \"mjml\", \"maizzle\"])\r\n .optional()\r\n .describe(\r\n \"Input format of the email source: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Controls which framework-specific fix snippets appear in the warnings.\"\r\n ),\r\n },\r\n annotations: {\r\n title: \"Preview Email\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async ({ html, clients, format }) => {\r\n const validClientIds = new Set(EMAIL_CLIENTS.map((c: { id: string }) => c.id));\r\n\r\n let transforms = transformForAllClients(html);\r\n if (clients) {\r\n const filter = clients.filter((c: string) => validClientIds.has(c));\r\n if (filter.length === 0) {\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n error: \"No valid client IDs provided\",\r\n validClientIds: Array.from(validClientIds),\r\n },\r\n null,\r\n 2\r\n ),\r\n },\r\n ],\r\n isError: true,\r\n };\r\n }\r\n transforms = transforms.filter((t: { clientId: string }) =>\r\n filter.includes(t.clientId)\r\n );\r\n }\r\n\r\n const warnings = analyzeEmail(html, toFramework(format));\r\n const scores = generateCompatibilityScore(warnings);\r\n\r\n const darkMode: Record<string, { html: string; warnings: CSSWarning[] }> =\r\n {};\r\n for (const t of transforms) {\r\n darkMode[t.clientId] = simulateDarkMode(t.html, t.clientId);\r\n }\r\n\r\n const scoreValues = Object.values(scores);\r\n const overallScore =\r\n scoreValues.length > 0\r\n ? Math.round(\r\n scoreValues.reduce(\r\n (a: number, b: { score: number }) => a + b.score,\r\n 0\r\n ) / scoreValues.length\r\n )\r\n : 0;\r\n\r\n // Generate plain text version for multipart emails\r\n const plainText = toPlainText ? toPlainText(html) : undefined;\r\n\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n overallScore,\r\n compatibilityScores: scores,\r\n cssWarnings: warnings.map((w: CSSWarning) => ({\r\n client: w.client,\r\n property: w.property,\r\n severity: w.severity,\r\n message: w.message,\r\n suggestion: w.suggestion,\r\n fix: w.fix,\r\n fixType: w.fixType,\r\n })),\r\n ...(plainText ? { plainText } : {}),\r\n clientCount: transforms.length,\r\n darkModeWarnings: Object.entries(darkMode).reduce(\r\n (\r\n acc: Record<string, number>,\r\n [clientId, dm]: [string, { warnings: CSSWarning[] }]\r\n ) => {\r\n if (dm.warnings.length > 0) {\r\n acc[clientId] = dm.warnings.length;\r\n }\r\n return acc;\r\n },\r\n {}\r\n ),\r\n },\r\n null,\r\n 2\r\n ),\r\n },\r\n ],\r\n };\r\n }\r\n);\r\n\r\n// ── Tool: audit_email ───────────────────────────────────────────\r\nserver.registerTool(\r\n \"audit_email\",\r\n {\r\n title: \"Audit Email\",\r\n description:\r\n \"Run ALL email checks in one call: CSS compatibility, spam scoring, link validation, accessibility audit, and image analysis. Returns a unified report. Use --skip to omit specific checks.\",\r\n inputSchema: {\r\n html: z.string().describe(\"The email HTML source code\"),\r\n format: z\r\n .enum([\"html\", \"jsx\", \"mjml\", \"maizzle\"])\r\n .optional()\r\n .describe(\r\n \"Input format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'.\"\r\n ),\r\n skip: z\r\n .array(z.enum([\"spam\", \"links\", \"accessibility\", \"images\", \"compatibility\"]))\r\n .optional()\r\n .describe(\r\n \"Array of checks to skip (e.g. ['spam', 'images']).\"\r\n ),\r\n },\r\n annotations: {\r\n title: \"Audit Email\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async ({ html, format, skip }) => {\r\n const report = auditEmail(html, {\r\n framework: toFramework(format),\r\n skip,\r\n });\r\n\r\n const scoreValues = Object.values(report.compatibility.scores);\r\n const overallCompatibility =\r\n scoreValues.length > 0\r\n ? Math.round(\r\n scoreValues.reduce(\r\n (a: number, b: { score: number }) => a + b.score,\r\n 0\r\n ) / scoreValues.length\r\n )\r\n : 0;\r\n\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n overallCompatibility,\r\n compatibility: {\r\n scores: report.compatibility.scores,\r\n warningCount: report.compatibility.warnings.length,\r\n warnings: report.compatibility.warnings.map((w: CSSWarning) => ({\r\n client: w.client,\r\n property: w.property,\r\n severity: w.severity,\r\n message: w.message,\r\n suggestion: w.suggestion,\r\n fix: w.fix,\r\n fixType: w.fixType,\r\n })),\r\n },\r\n spam: report.spam,\r\n links: report.links,\r\n accessibility: report.accessibility,\r\n images: report.images,\r\n },\r\n null,\r\n 2\r\n ),\r\n },\r\n ],\r\n };\r\n }\r\n);\r\n\r\n// ── Tool: analyze_email ─────────────────────────────────────────\r\nserver.registerTool(\r\n \"analyze_email\",\r\n {\r\n title: \"Analyze Email\",\r\n description:\r\n \"CSS compatibility analysis with quality reports — returns warnings, per-client scores, plus spam, link, accessibility, and image analysis. For compatibility-only results, use skip. For full transforms + dark mode, use preview_email.\",\r\n inputSchema: {\r\n html: z.string().describe(\"The email HTML source code\"),\r\n format: z\r\n .enum([\"html\", \"jsx\", \"mjml\", \"maizzle\"])\r\n .optional()\r\n .describe(\r\n \"Input format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Determines which framework-specific fix snippets are returned.\"\r\n ),\r\n },\r\n annotations: {\r\n title: \"Analyze Email\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async ({ html, format }) => {\r\n const report = auditEmail(html, { framework: toFramework(format) });\r\n\r\n const scoreValues = Object.values(report.compatibility.scores);\r\n const overallScore =\r\n scoreValues.length > 0\r\n ? Math.round(\r\n scoreValues.reduce(\r\n (a: number, b: { score: number }) => a + b.score,\r\n 0\r\n ) / scoreValues.length\r\n )\r\n : 0;\r\n\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n overallScore,\r\n scores: report.compatibility.scores,\r\n warningCount: report.compatibility.warnings.length,\r\n warnings: report.compatibility.warnings.map((w: CSSWarning) => ({\r\n client: w.client,\r\n property: w.property,\r\n severity: w.severity,\r\n message: w.message,\r\n suggestion: w.suggestion,\r\n fix: w.fix,\r\n fixType: w.fixType,\r\n })),\r\n spam: report.spam,\r\n links: report.links,\r\n accessibility: report.accessibility,\r\n images: report.images,\r\n },\r\n null,\r\n 2\r\n ),\r\n },\r\n ],\r\n };\r\n }\r\n);\r\n\r\n// ── Tool: fix_email ─────────────────────────────────────────────\r\nserver.registerTool(\r\n \"fix_email\",\r\n {\r\n title: \"Fix Email\",\r\n description:\r\n \"Generate a structured fix prompt for email compatibility issues. Returns a markdown prompt with the original code, all detected issues (with fix type: CSS or structural), fix snippets, and format-specific instructions. The AI assistant can then apply these fixes directly. Use after preview_email or analyze_email to fix the issues found.\",\r\n inputSchema: {\r\n html: z.string().describe(\"The email HTML source code to fix\"),\r\n format: z\r\n .enum([\"html\", \"jsx\", \"mjml\", \"maizzle\"])\r\n .optional()\r\n .describe(\r\n \"Input format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Controls the fix syntax in the prompt.\"\r\n ),\r\n scope: z\r\n .enum([\"all\", \"current\"])\r\n .optional()\r\n .describe(\r\n \"Fix scope: 'all' (default) fixes for all clients, 'current' fixes for a single client (requires selectedClientId).\"\r\n ),\r\n selectedClientId: z\r\n .string()\r\n .optional()\r\n .describe(\r\n \"Client ID to scope fixes to (e.g. 'outlook-windows'). Only used when scope is 'current'.\"\r\n ),\r\n },\r\n annotations: {\r\n title: \"Fix Email\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async ({ html, format, scope, selectedClientId }) => {\r\n const framework = toFramework(format);\r\n const fixScope = scope === \"current\" ? \"current\" : \"all\";\r\n const inputFormat = format || \"html\";\r\n\r\n const warnings = analyzeEmail(html, framework);\r\n const scores = generateCompatibilityScore(warnings);\r\n\r\n if (warnings.length === 0) {\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n { message: \"No compatibility issues found — nothing to fix.\", overallScore: 100 },\r\n null,\r\n 2\r\n ),\r\n },\r\n ],\r\n };\r\n }\r\n\r\n // Estimate tokens\r\n const estimate = await estimateAiFixTokens({\r\n originalHtml: html,\r\n warnings,\r\n scores,\r\n scope: fixScope,\r\n selectedClientId,\r\n format: inputFormat,\r\n });\r\n\r\n // Generate the fix prompt\r\n const prompt = generateFixPrompt({\r\n originalHtml: html,\r\n warnings,\r\n scores,\r\n scope: fixScope,\r\n selectedClientId,\r\n format: inputFormat,\r\n });\r\n\r\n const structural = structuralWarnings(warnings);\r\n\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n {\r\n totalWarnings: warnings.length,\r\n structuralWarnings: structural.length,\r\n cssWarnings: warnings.length - structural.length,\r\n tokenEstimate: {\r\n inputTokens: estimate.inputTokens,\r\n estimatedOutputTokens: estimate.estimatedOutputTokens,\r\n truncated: estimate.truncated,\r\n },\r\n note: \"Apply the fixes in the prompt below to the email code. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes.\",\r\n },\r\n null,\r\n 2\r\n ),\r\n },\r\n {\r\n type: \"text\" as const,\r\n text: prompt,\r\n },\r\n ],\r\n };\r\n }\r\n);\r\n\r\n// ── Tool: list_clients ──────────────────────────────────────────\r\nserver.registerTool(\r\n \"list_clients\",\r\n {\r\n title: \"List Email Clients\",\r\n description:\r\n \"List all 12 supported email client IDs, display names, categories, rendering engines, and dark mode support.\",\r\n annotations: {\r\n title: \"List Email Clients\",\r\n readOnlyHint: true,\r\n destructiveHint: false,\r\n idempotentHint: true,\r\n openWorldHint: false,\r\n },\r\n },\r\n async () => {\r\n return {\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: JSON.stringify(\r\n EMAIL_CLIENTS.map(\r\n (c: {\r\n id: string;\r\n name: string;\r\n category: string;\r\n engine: string;\r\n darkModeSupport: boolean;\r\n }) => ({\r\n id: c.id,\r\n name: c.name,\r\n category: c.category,\r\n engine: c.engine,\r\n darkModeSupport: c.darkModeSupport,\r\n })\r\n ),\r\n null,\r\n 2\r\n ),\r\n },\r\n ],\r\n };\r\n }\r\n);\r\n\r\n// ── Start ────────────────────────────────────────────────────────\r\nconst transport = new StdioServerTransport();\r\nawait server.connect(transport);\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.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 {\n createSession,\n analyzeEmail,\n generateCompatibilityScore,\n generateFixPrompt,\n estimateAiFixTokens,\n structuralWarnings,\n diffResults,\n toPlainText,\n EMAIL_CLIENTS,\n MAX_HTML_SIZE,\n type Framework,\n type CSSWarning,\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\nconst formatEnum = z.enum([\"html\", \"jsx\", \"mjml\", \"maizzle\"]).optional();\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.4.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 15 email clients (Gmail, Outlook, Apple Mail, Yahoo, Samsung, Thunderbird, HEY, Superhuman), analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.\",\n inputSchema: {\n html: z.string().describe(\"The email HTML source code\"),\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(\"Input format: 'html' (default), 'jsx', 'mjml', or 'maizzle'\"),\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 }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n\n const validClientIds = new Set(EMAIL_CLIENTS.map((c) => c.id));\n const framework = toFramework(format);\n const session = createSession(html, { framework });\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) => session.transformForClient(c));\n } else {\n transforms = session.transformForAllClients();\n }\n\n const warnings = session.analyze();\n const scores = session.score(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] = session.simulateDarkMode(t.clientId);\n }\n\n const scoreValues = Object.values(scores);\n const overallScore =\n scoreValues.length > 0\n ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length)\n : 0;\n\n const plainText = toPlainText(html);\n\n const result: Record<string, unknown> = {\n overallScore,\n compatibilityScores: scores,\n cssWarnings: warnings.map((w) => ({\n client: w.client,\n property: w.property,\n severity: w.severity,\n message: w.message,\n suggestion: w.suggestion,\n fix: w.fix,\n fixType: w.fixType,\n })),\n inboxPreview,\n sizeReport,\n ...(plainText ? { plainText } : {}),\n clientCount: transforms.length,\n darkModeWarnings: Object.entries(darkMode).reduce(\n (acc, [clientId, dm]) => {\n if (dm.warnings.length > 0) acc[clientId] = dm.warnings.length;\n return acc;\n },\n {} as Record<string, number>,\n ),\n };\n\n // Contextual tip: only when no API key and score indicates issues\n if (!config.isHosted && overallScore < 90) {\n result.tip =\n \"Capture real screenshots to verify these issues across clients. Add EMAILENS_API_KEY to your MCP config \\u2192 emailens.dev/settings/api-keys?ref=mcp\";\n }\n\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] };\n },\n);\n\n// ── Local Tool: analyze_email (CSS-only, reverted from audit) ──────\n\nserver.registerTool(\n \"analyze_email\",\n {\n title: \"Analyze Email\",\n description:\n \"Quick CSS compatibility analysis — returns warnings and per-client scores. Use audit_email for full quality report (spam, links, a11y, images, etc.).\",\n inputSchema: {\n html: z.string().describe(\"The email HTML source code\"),\n format: formatEnum.describe(\"Input format for framework-specific fix snippets\"),\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 }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n\n const warnings = analyzeEmail(html, toFramework(format));\n const scores = generateCompatibilityScore(warnings);\n\n const scoreValues = Object.values(scores);\n const overallScore =\n scoreValues.length > 0\n ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length)\n : 0;\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n overallScore,\n scores,\n warningCount: warnings.length,\n warnings: 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 },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Local Tool: audit_email ────────────────────────────────────────\n\nserver.registerTool(\n \"audit_email\",\n {\n title: \"Audit Email\",\n description:\n \"Comprehensive email quality audit — CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), and template variables. Use skip to omit specific checks.\",\n inputSchema: {\n html: z.string().describe(\"The email HTML source code\"),\n format: formatEnum.describe(\"Input format for framework-specific fix snippets\"),\n skip: z\n .array(z.enum([\"spam\", \"links\", \"accessibility\", \"images\", \"compatibility\", \"inboxPreview\", \"size\", \"templateVariables\"]))\n .optional()\n .describe(\"Checks to skip (e.g. ['spam', 'images'])\"),\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 }) => {\n const sizeError = validateHtmlSize(html);\n if (sizeError) return sizeError;\n\n const session = createSession(html, { framework: toFramework(format) });\n const report = session.audit({ skip });\n\n const scoreValues = Object.values(report.compatibility.scores);\n const overallCompatibility =\n scoreValues.length > 0\n ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length)\n : 0;\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 warningCount: report.compatibility.warnings.length,\n warnings: report.compatibility.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 },\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 },\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 HTML source code to fix\"),\n format: formatEnum.describe(\"Input format — controls fix syntax\"),\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 const framework = toFramework(format);\n const fixScope = scope === \"current\" ? \"current\" : \"all\";\n const inputFormat = format || \"html\";\n\n const warnings = analyzeEmail(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 = structuralWarnings(warnings);\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 all 15 supported email clients 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 HTML\"),\n after: z.string().describe(\"Modified email HTML\"),\n format: formatEnum.describe(\"Input format for framework-specific analysis\"),\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 framework = toFramework(format);\n\n const beforeWarnings = analyzeEmail(before, framework);\n const beforeScores = generateCompatibilityScore(beforeWarnings);\n\n const afterWarnings = analyzeEmail(after, 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 15 clients (Gmail, Outlook, Apple Mail, etc.) with light and dark mode variants. Screenshots are rendered in real browsers and hosted on CDN. Requires EMAILENS_API_KEY env var \\u2014 free plan at emailens.dev?ref=mcp.\",\n inputSchema: {\n html: z.string().describe(\"The email HTML source code\"),\n format: formatEnum.describe(\"Input format\"),\n clients: z.array(z.string()).optional().describe(\"Filter to specific client IDs\"),\n modes: z\n .array(z.enum([\"light\", \"dark\"]))\n .optional()\n .describe(\"Screenshot modes (default: ['light'])\"),\n title: z.string().optional().describe(\"Name for the hosted preview\"),\n },\n annotations: {\n title: \"Capture Screenshots\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ html, format, clients, modes, title }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"capture_screenshots\");\n }\n\n const opts = { apiKey: config.apiKey!, apiUrl: config.apiUrl };\n\n // Step 1: Create hosted preview\n const previewResult = await apiRequest<{\n id: string;\n overallScore: number;\n }>(\"/api/preview\", {\n ...opts,\n body: { html, format, clients, title },\n timeoutMs: 30_000,\n });\n\n if (previewResult.isError) {\n return mcpError(previewResult.error!);\n }\n\n const previewId = previewResult.data!.id;\n\n // Step 2: Capture screenshots (with one retry on timeout)\n let screenshotResult = await apiRequest<{\n screenshots: Record<string, string>;\n screenshotStatus: string;\n captured: string[];\n failed: string[];\n }>(`/api/preview/${previewId}/screenshots`, {\n ...opts,\n body: { clients, modes },\n timeoutMs: 120_000,\n });\n\n // Retry once on timeout\n if (screenshotResult.isError && screenshotResult.error?.includes(\"timed out\")) {\n screenshotResult = await apiRequest(`/api/preview/${previewId}/screenshots`, {\n ...opts,\n body: { clients, modes },\n timeoutMs: 120_000,\n });\n }\n\n // If screenshots still failed, return preview analysis with error note\n if (screenshotResult.isError) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n previewId,\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\n overallScore: previewResult.data!.overallScore,\n screenshots: {},\n screenshotStatus: \"failed\",\n captured: [],\n failed: [],\n note: `Preview created but screenshot capture failed: ${screenshotResult.error}. View the full preview at the URL above.`,\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n const ss = screenshotResult.data!;\n const result: Record<string, unknown> = {\n previewId,\n previewUrl: `${config.apiUrl}/preview/${previewId}`,\n overallScore: previewResult.data!.overallScore,\n screenshots: ss.screenshots,\n screenshotStatus: ss.screenshotStatus,\n captured: ss.captured,\n failed: ss.failed,\n };\n\n if (ss.failed?.length > 0) {\n result.note = `${ss.failed.length} of ${(ss.captured?.length ?? 0) + ss.failed.length} screenshots failed: ${ss.failed.join(\", \")}`;\n }\n\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result, null, 2) }] };\n },\n);\n\n// ── Hosted Tool: share_preview ─────────────────────────────────────\n\nserver.registerTool(\n \"share_preview\",\n {\n title: \"Share Preview\",\n description:\n \"Create a shareable link for an email preview. Recipients see the full analysis without needing an account. Requires EMAILENS_API_KEY env var and Dev plan ($9/mo).\",\n inputSchema: {\n html: z.string().describe(\"The email HTML source code\"),\n title: z.string().optional().describe(\"Display title for the share page\"),\n format: formatEnum.describe(\"Input format\"),\n },\n annotations: {\n title: \"Share Preview\",\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n },\n },\n async ({ html, title, format }) => {\n if (!config.isHosted) {\n return noApiKeyError(\"share_preview\", \"Shareable links require the Dev plan ($9/mo).\");\n }\n\n const result = await apiRequest<{ id: string; expiresAt: string | null }>(\"/api/share\", {\n apiKey: config.apiKey!,\n apiUrl: config.apiUrl,\n body: { html, title, format },\n });\n\n if (result.isError) {\n return mcpError(result.error!);\n }\n\n return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(\n {\n shareUrl: `${config.apiUrl}/share/${result.data!.id}`,\n expiresAt: result.data!.expiresAt,\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n);\n\n// ── Start ────────────────────────────────────────────────────────────\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n","export const config = {\n get apiKey(): string | null {\n return process.env.EMAILENS_API_KEY ?? null;\n },\n get apiUrl(): string {\n return process.env.EMAILENS_API_URL ?? \"https://emailens.dev\";\n },\n get isHosted(): boolean {\n return this.apiKey !== null;\n },\n};\n","export interface ApiRequestOptions {\n apiKey: string;\n apiUrl: string;\n method?: \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\";\n body?: unknown;\n timeoutMs?: number;\n}\n\nexport interface ApiResult<T = unknown> {\n data?: T;\n isError?: true;\n error?: string;\n}\n\nexport async function apiRequest<T = unknown>(\n path: string,\n options: ApiRequestOptions,\n): Promise<ApiResult<T>> {\n const { apiKey, apiUrl, method = \"GET\", body, timeoutMs = 30_000 } = options;\n const url = `${apiUrl}${path}`;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: method !== \"GET\" ? method : body ? \"POST\" : \"GET\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: body ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n if (response.status === 401) {\n return {\n isError: true,\n error: \"Invalid or revoked API key. Generate a new one at emailens.dev/settings/api-keys?ref=mcp\",\n };\n }\n\n if (response.status === 403) {\n const data = (await response.json().catch(() => ({}))) as Record<string, unknown>;\n return {\n isError: true,\n error: data.upgrade\n ? \"This feature requires a plan upgrade. Visit emailens.dev/pricing?ref=mcp\"\n : \"Access denied. Check your plan at emailens.dev/pricing?ref=mcp\",\n };\n }\n\n if (response.status === 429) {\n const data = (await response.json().catch(() => ({}))) as Record<string, number>;\n return {\n isError: true,\n error: `Daily quota exceeded${data.limit ? ` (${data.limit - (data.remaining ?? 0)}/${data.limit})` : \"\"}. Resets tomorrow. Upgrade at emailens.dev/pricing?ref=mcp`,\n };\n }\n\n if (!response.ok) {\n const data = (await response.json().catch(() => ({}))) as Record<string, unknown>;\n return { isError: true, error: (data.error as string) ?? `API error: ${response.status}` };\n }\n\n const data = (await response.json()) as T;\n return { data };\n } catch (err) {\n if ((err as Error).name === \"AbortError\") {\n return { isError: true, error: `Request timed out after ${timeoutMs / 1000}s. Try again or check emailens.dev status.` };\n }\n return { isError: true, error: `Network error: ${(err as Error).message}` };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function mcpError(message: string) {\n return {\n content: [{ type: \"text\" as const, text: message }],\n isError: true,\n };\n}\n\nexport function noApiKeyError(toolName: string, extraInfo?: string) {\n const lines = [\n `${toolName} requires an Emailens API key.`,\n \"\",\n \"1. Sign up free at emailens.dev?ref=mcp\",\n \"2. Create a key at emailens.dev/settings/api-keys?ref=mcp\",\n \"3. Add to your MCP config:\",\n \"\",\n ' \"env\": { \"EMAILENS_API_KEY\": \"ek_live_...\" }',\n ];\n if (extraInfo) {\n lines.push(\"\", extraInfo);\n } else {\n lines.push(\"\", \"Free plan includes 30 previews/day (each preview can include screenshots).\");\n }\n return mcpError(lines.join(\"\\n\"));\n}\n"],"mappings":"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","names":["data"]}
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@emailens/mcp",
3
- "version": "0.3.3",
4
- "description": "MCP server for email compatibility analysis — preview, analyze, fix, and list email clients via Claude or any MCP-compatible tool.",
3
+ "mcpName": "io.github.emailens/mcp",
4
+ "version": "0.4.1",
5
+ "description": "MCP server for email compatibility analysis — preview, analyze, fix, diff, and capture screenshots across 15 email clients via Claude or any MCP-compatible tool.",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "emailens-mcp": "./dist/index.js"
@@ -26,7 +27,7 @@
26
27
  "prepublishOnly": "bun run build"
27
28
  },
28
29
  "dependencies": {
29
- "@emailens/engine": "^0.8.6",
30
+ "@emailens/engine": "^0.9.1",
30
31
  "@modelcontextprotocol/sdk": "^1.12.0",
31
32
  "zod": "^3.24.0"
32
33
  },