@emailens/mcp 0.3.2 → 0.4.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 +179 -183
- package/dist/index.js +455 -162
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,183 +1,179 @@
|
|
|
1
|
-
# @emailens/mcp
|
|
2
|
-
|
|
3
|
-
MCP
|
|
4
|
-
|
|
5
|
-
Built on
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Add to
|
|
18
|
-
|
|
19
|
-
```json
|
|
20
|
-
{
|
|
21
|
-
"mcpServers": {
|
|
22
|
-
"emailens": {
|
|
23
|
-
"command": "npx",
|
|
24
|
-
"args": ["-y", "@emailens/mcp"]
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
|
82
|
-
|---|---|---|---|
|
|
83
|
-
| `html` | string | Yes |
|
|
84
|
-
| `format` | enum | No | Input format
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
|
127
|
-
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
bun run build
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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,42 +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
|
-
|
|
8
|
+
createSession,
|
|
10
9
|
analyzeEmail,
|
|
11
10
|
generateCompatibilityScore,
|
|
12
|
-
simulateDarkMode,
|
|
13
11
|
generateFixPrompt,
|
|
14
12
|
estimateAiFixTokens,
|
|
15
|
-
auditEmail,
|
|
16
13
|
structuralWarnings,
|
|
17
|
-
|
|
14
|
+
diffResults,
|
|
15
|
+
toPlainText,
|
|
16
|
+
EMAIL_CLIENTS,
|
|
17
|
+
MAX_HTML_SIZE
|
|
18
18
|
} from "@emailens/engine";
|
|
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
|
+
};
|
|
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;
|
|
19
110
|
function toFramework(format) {
|
|
20
111
|
if (format === "jsx" || format === "mjml" || format === "maizzle") return format;
|
|
21
112
|
return void 0;
|
|
22
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
|
+
}
|
|
23
121
|
var server = new McpServer({
|
|
24
122
|
name: "emailens",
|
|
25
|
-
version: "0.
|
|
123
|
+
version: "0.4.0"
|
|
26
124
|
});
|
|
27
125
|
server.registerTool(
|
|
28
126
|
"preview_email",
|
|
29
127
|
{
|
|
30
128
|
title: "Preview Email",
|
|
31
|
-
description: "
|
|
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.",
|
|
32
130
|
inputSchema: {
|
|
33
131
|
html: z.string().describe("The email HTML source code"),
|
|
34
|
-
clients: z.array(z.string()).optional().describe(
|
|
35
|
-
|
|
36
|
-
),
|
|
37
|
-
format: z.enum(["html", "jsx", "mjml", "maizzle"]).optional().describe(
|
|
38
|
-
"Input format of the email source: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Controls which framework-specific fix snippets appear in the warnings."
|
|
39
|
-
)
|
|
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'")
|
|
40
134
|
},
|
|
41
135
|
annotations: {
|
|
42
136
|
title: "Preview Email",
|
|
@@ -47,45 +141,88 @@ server.registerTool(
|
|
|
47
141
|
}
|
|
48
142
|
},
|
|
49
143
|
async ({ html, clients, format }) => {
|
|
144
|
+
const sizeError = validateHtmlSize(html);
|
|
145
|
+
if (sizeError) return sizeError;
|
|
50
146
|
const validClientIds = new Set(EMAIL_CLIENTS.map((c) => c.id));
|
|
51
|
-
|
|
147
|
+
const framework = toFramework(format);
|
|
148
|
+
const session = createSession(html, { framework });
|
|
149
|
+
let transforms;
|
|
52
150
|
if (clients) {
|
|
53
151
|
const filter = clients.filter((c) => validClientIds.has(c));
|
|
54
152
|
if (filter.length === 0) {
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
type: "text",
|
|
59
|
-
text: JSON.stringify(
|
|
60
|
-
{
|
|
61
|
-
error: "No valid client IDs provided",
|
|
62
|
-
validClientIds: Array.from(validClientIds)
|
|
63
|
-
},
|
|
64
|
-
null,
|
|
65
|
-
2
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
],
|
|
69
|
-
isError: true
|
|
70
|
-
};
|
|
153
|
+
return mcpError(
|
|
154
|
+
JSON.stringify({ error: "No valid client IDs provided", validClientIds: Array.from(validClientIds) }, null, 2)
|
|
155
|
+
);
|
|
71
156
|
}
|
|
72
|
-
transforms =
|
|
73
|
-
|
|
74
|
-
);
|
|
157
|
+
transforms = filter.map((c) => session.transformForClient(c));
|
|
158
|
+
} else {
|
|
159
|
+
transforms = session.transformForAllClients();
|
|
75
160
|
}
|
|
76
|
-
const warnings =
|
|
77
|
-
const scores =
|
|
161
|
+
const warnings = session.analyze();
|
|
162
|
+
const scores = session.score(warnings);
|
|
163
|
+
const inboxPreview = session.extractInboxPreview();
|
|
164
|
+
const sizeReport = session.checkSize();
|
|
78
165
|
const darkMode = {};
|
|
79
166
|
for (const t of transforms) {
|
|
80
|
-
darkMode[t.clientId] = simulateDarkMode(t.
|
|
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";
|
|
81
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);
|
|
82
224
|
const scoreValues = Object.values(scores);
|
|
83
|
-
const overallScore = scoreValues.length > 0 ? Math.round(
|
|
84
|
-
scoreValues.reduce(
|
|
85
|
-
(a, b) => a + b.score,
|
|
86
|
-
0
|
|
87
|
-
) / scoreValues.length
|
|
88
|
-
) : 0;
|
|
225
|
+
const overallScore = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
|
|
89
226
|
return {
|
|
90
227
|
content: [
|
|
91
228
|
{
|
|
@@ -93,8 +230,9 @@ server.registerTool(
|
|
|
93
230
|
text: JSON.stringify(
|
|
94
231
|
{
|
|
95
232
|
overallScore,
|
|
96
|
-
|
|
97
|
-
|
|
233
|
+
scores,
|
|
234
|
+
warningCount: warnings.length,
|
|
235
|
+
warnings: warnings.map((w) => ({
|
|
98
236
|
client: w.client,
|
|
99
237
|
property: w.property,
|
|
100
238
|
severity: w.severity,
|
|
@@ -102,17 +240,7 @@ server.registerTool(
|
|
|
102
240
|
suggestion: w.suggestion,
|
|
103
241
|
fix: w.fix,
|
|
104
242
|
fixType: w.fixType
|
|
105
|
-
}))
|
|
106
|
-
clientCount: transforms.length,
|
|
107
|
-
darkModeWarnings: Object.entries(darkMode).reduce(
|
|
108
|
-
(acc, [clientId, dm]) => {
|
|
109
|
-
if (dm.warnings.length > 0) {
|
|
110
|
-
acc[clientId] = dm.warnings.length;
|
|
111
|
-
}
|
|
112
|
-
return acc;
|
|
113
|
-
},
|
|
114
|
-
{}
|
|
115
|
-
)
|
|
243
|
+
}))
|
|
116
244
|
},
|
|
117
245
|
null,
|
|
118
246
|
2
|
|
@@ -126,15 +254,11 @@ server.registerTool(
|
|
|
126
254
|
"audit_email",
|
|
127
255
|
{
|
|
128
256
|
title: "Audit Email",
|
|
129
|
-
description: "
|
|
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.",
|
|
130
258
|
inputSchema: {
|
|
131
259
|
html: z.string().describe("The email HTML source code"),
|
|
132
|
-
format:
|
|
133
|
-
|
|
134
|
-
),
|
|
135
|
-
skip: z.array(z.enum(["spam", "links", "accessibility", "images", "compatibility"])).optional().describe(
|
|
136
|
-
"Array of checks to skip (e.g. ['spam', 'images'])."
|
|
137
|
-
)
|
|
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'])")
|
|
138
262
|
},
|
|
139
263
|
annotations: {
|
|
140
264
|
title: "Audit Email",
|
|
@@ -145,17 +269,12 @@ server.registerTool(
|
|
|
145
269
|
}
|
|
146
270
|
},
|
|
147
271
|
async ({ html, format, skip }) => {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
});
|
|
272
|
+
const sizeError = validateHtmlSize(html);
|
|
273
|
+
if (sizeError) return sizeError;
|
|
274
|
+
const session = createSession(html, { framework: toFramework(format) });
|
|
275
|
+
const report = session.audit({ skip });
|
|
152
276
|
const scoreValues = Object.values(report.compatibility.scores);
|
|
153
|
-
const overallCompatibility = scoreValues.length > 0 ? Math.round(
|
|
154
|
-
scoreValues.reduce(
|
|
155
|
-
(a, b) => a + b.score,
|
|
156
|
-
0
|
|
157
|
-
) / scoreValues.length
|
|
158
|
-
) : 0;
|
|
277
|
+
const overallCompatibility = scoreValues.length > 0 ? Math.round(scoreValues.reduce((a, b) => a + b.score, 0) / scoreValues.length) : 0;
|
|
159
278
|
return {
|
|
160
279
|
content: [
|
|
161
280
|
{
|
|
@@ -179,66 +298,10 @@ server.registerTool(
|
|
|
179
298
|
spam: report.spam,
|
|
180
299
|
links: report.links,
|
|
181
300
|
accessibility: report.accessibility,
|
|
182
|
-
images: report.images
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
]
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
);
|
|
192
|
-
server.registerTool(
|
|
193
|
-
"analyze_email",
|
|
194
|
-
{
|
|
195
|
-
title: "Analyze Email",
|
|
196
|
-
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.",
|
|
197
|
-
inputSchema: {
|
|
198
|
-
html: z.string().describe("The email HTML source code"),
|
|
199
|
-
format: z.enum(["html", "jsx", "mjml", "maizzle"]).optional().describe(
|
|
200
|
-
"Input format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle'. Determines which framework-specific fix snippets are returned."
|
|
201
|
-
)
|
|
202
|
-
},
|
|
203
|
-
annotations: {
|
|
204
|
-
title: "Analyze Email",
|
|
205
|
-
readOnlyHint: true,
|
|
206
|
-
destructiveHint: false,
|
|
207
|
-
idempotentHint: true,
|
|
208
|
-
openWorldHint: false
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
async ({ html, format }) => {
|
|
212
|
-
const report = auditEmail(html, { framework: toFramework(format) });
|
|
213
|
-
const scoreValues = Object.values(report.compatibility.scores);
|
|
214
|
-
const overallScore = scoreValues.length > 0 ? Math.round(
|
|
215
|
-
scoreValues.reduce(
|
|
216
|
-
(a, b) => a + b.score,
|
|
217
|
-
0
|
|
218
|
-
) / scoreValues.length
|
|
219
|
-
) : 0;
|
|
220
|
-
return {
|
|
221
|
-
content: [
|
|
222
|
-
{
|
|
223
|
-
type: "text",
|
|
224
|
-
text: JSON.stringify(
|
|
225
|
-
{
|
|
226
|
-
overallScore,
|
|
227
|
-
scores: report.compatibility.scores,
|
|
228
|
-
warningCount: report.compatibility.warnings.length,
|
|
229
|
-
warnings: report.compatibility.warnings.map((w) => ({
|
|
230
|
-
client: w.client,
|
|
231
|
-
property: w.property,
|
|
232
|
-
severity: w.severity,
|
|
233
|
-
message: w.message,
|
|
234
|
-
suggestion: w.suggestion,
|
|
235
|
-
fix: w.fix,
|
|
236
|
-
fixType: w.fixType
|
|
237
|
-
})),
|
|
238
|
-
spam: report.spam,
|
|
239
|
-
links: report.links,
|
|
240
|
-
accessibility: report.accessibility,
|
|
241
|
-
images: report.images
|
|
301
|
+
images: report.images,
|
|
302
|
+
inboxPreview: report.inboxPreview,
|
|
303
|
+
size: report.size,
|
|
304
|
+
templateVariables: report.templateVariables
|
|
242
305
|
},
|
|
243
306
|
null,
|
|
244
307
|
2
|
|
@@ -252,18 +315,12 @@ server.registerTool(
|
|
|
252
315
|
"fix_email",
|
|
253
316
|
{
|
|
254
317
|
title: "Fix Email",
|
|
255
|
-
description: "Generate a structured fix prompt for email compatibility issues. Returns
|
|
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.",
|
|
256
319
|
inputSchema: {
|
|
257
320
|
html: z.string().describe("The email HTML source code to fix"),
|
|
258
|
-
format:
|
|
259
|
-
|
|
260
|
-
)
|
|
261
|
-
scope: z.enum(["all", "current"]).optional().describe(
|
|
262
|
-
"Fix scope: 'all' (default) fixes for all clients, 'current' fixes for a single client (requires selectedClientId)."
|
|
263
|
-
),
|
|
264
|
-
selectedClientId: z.string().optional().describe(
|
|
265
|
-
"Client ID to scope fixes to (e.g. 'outlook-windows'). Only used when scope is 'current'."
|
|
266
|
-
)
|
|
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')")
|
|
267
324
|
},
|
|
268
325
|
annotations: {
|
|
269
326
|
title: "Fix Email",
|
|
@@ -274,6 +331,8 @@ server.registerTool(
|
|
|
274
331
|
}
|
|
275
332
|
},
|
|
276
333
|
async ({ html, format, scope, selectedClientId }) => {
|
|
334
|
+
const sizeError = validateHtmlSize(html);
|
|
335
|
+
if (sizeError) return sizeError;
|
|
277
336
|
const framework = toFramework(format);
|
|
278
337
|
const fixScope = scope === "current" ? "current" : "all";
|
|
279
338
|
const inputFormat = format || "html";
|
|
@@ -284,11 +343,7 @@ server.registerTool(
|
|
|
284
343
|
content: [
|
|
285
344
|
{
|
|
286
345
|
type: "text",
|
|
287
|
-
text: JSON.stringify(
|
|
288
|
-
{ message: "No compatibility issues found \u2014 nothing to fix.", overallScore: 100 },
|
|
289
|
-
null,
|
|
290
|
-
2
|
|
291
|
-
)
|
|
346
|
+
text: JSON.stringify({ message: "No compatibility issues found \u2014 nothing to fix.", overallScore: 100 }, null, 2)
|
|
292
347
|
}
|
|
293
348
|
]
|
|
294
349
|
};
|
|
@@ -324,16 +379,13 @@ server.registerTool(
|
|
|
324
379
|
estimatedOutputTokens: estimate.estimatedOutputTokens,
|
|
325
380
|
truncated: estimate.truncated
|
|
326
381
|
},
|
|
327
|
-
note: "Apply the fixes in the prompt below
|
|
382
|
+
note: "Apply the fixes in the prompt below. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes."
|
|
328
383
|
},
|
|
329
384
|
null,
|
|
330
385
|
2
|
|
331
386
|
)
|
|
332
387
|
},
|
|
333
|
-
{
|
|
334
|
-
type: "text",
|
|
335
|
-
text: prompt
|
|
336
|
-
}
|
|
388
|
+
{ type: "text", text: prompt }
|
|
337
389
|
]
|
|
338
390
|
};
|
|
339
391
|
}
|
|
@@ -342,7 +394,7 @@ server.registerTool(
|
|
|
342
394
|
"list_clients",
|
|
343
395
|
{
|
|
344
396
|
title: "List Email Clients",
|
|
345
|
-
description: "List all
|
|
397
|
+
description: "List all 15 supported email clients with IDs, names, engines, and dark mode support.",
|
|
346
398
|
annotations: {
|
|
347
399
|
title: "List Email Clients",
|
|
348
400
|
readOnlyHint: true,
|
|
@@ -357,15 +409,256 @@ server.registerTool(
|
|
|
357
409
|
{
|
|
358
410
|
type: "text",
|
|
359
411
|
text: JSON.stringify(
|
|
360
|
-
EMAIL_CLIENTS.map(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
+
},
|
|
369
662
|
null,
|
|
370
663
|
2
|
|
371
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\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 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 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,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emailens/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for email compatibility analysis — preview, analyze, fix, and
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"emailens-mcp": "./dist/index.js"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"prepublishOnly": "bun run build"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@emailens/engine": "^0.
|
|
29
|
+
"@emailens/engine": "^0.9.1",
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
31
31
|
"zod": "^3.24.0"
|
|
32
32
|
},
|