@emailens/mcp 0.12.1 → 0.13.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 +336 -328
- package/dist/index.js +117 -53
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -123,7 +123,7 @@ import {
|
|
|
123
123
|
toPlainText,
|
|
124
124
|
checkDesignRetention,
|
|
125
125
|
EMAIL_CLIENTS as EMAIL_CLIENTS2,
|
|
126
|
-
MAX_HTML_SIZE
|
|
126
|
+
MAX_HTML_SIZE as MAX_HTML_SIZE2
|
|
127
127
|
} from "@emailens/engine";
|
|
128
128
|
|
|
129
129
|
// src/config.ts
|
|
@@ -167,6 +167,46 @@ function firstDraft(html) {
|
|
|
167
167
|
return html;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// src/source.ts
|
|
171
|
+
import { readFile, realpath, stat } from "fs/promises";
|
|
172
|
+
import { extname } from "path";
|
|
173
|
+
import { MAX_HTML_SIZE } from "@emailens/engine";
|
|
174
|
+
var EXTENSIONS = {
|
|
175
|
+
".html": void 0,
|
|
176
|
+
".htm": void 0,
|
|
177
|
+
".mjml": "mjml",
|
|
178
|
+
".jsx": "jsx",
|
|
179
|
+
".tsx": "jsx",
|
|
180
|
+
".vue": "maizzle"
|
|
181
|
+
};
|
|
182
|
+
async function readSource(html, path, format, names = { html: "html", path: "path" }) {
|
|
183
|
+
if (html === void 0 === (path === void 0)) {
|
|
184
|
+
return { ok: false, message: `Pass exactly one of \`${names.html}\` (the source) or \`${names.path}\` (a file holding it).` };
|
|
185
|
+
}
|
|
186
|
+
if (html !== void 0) return { ok: true, html, format };
|
|
187
|
+
let real;
|
|
188
|
+
try {
|
|
189
|
+
real = await realpath(path);
|
|
190
|
+
} catch {
|
|
191
|
+
return { ok: false, message: `No file at ${path}. Pass an absolute path, or the source itself as \`${names.html}\`.` };
|
|
192
|
+
}
|
|
193
|
+
const ext = extname(real).toLowerCase();
|
|
194
|
+
if (!(ext in EXTENSIONS)) {
|
|
195
|
+
return { ok: false, message: `${path} is not an email source. Accepted: ${Object.keys(EXTENSIONS).join(", ")}.` };
|
|
196
|
+
}
|
|
197
|
+
const info = await stat(real);
|
|
198
|
+
if (!info.isFile()) return { ok: false, message: `${path} is not a file.` };
|
|
199
|
+
if (info.size > MAX_HTML_SIZE) {
|
|
200
|
+
return { ok: false, message: `${path} exceeds the ${Math.round(MAX_HTML_SIZE / 1024)}KB limit.` };
|
|
201
|
+
}
|
|
202
|
+
return { ok: true, html: await readFile(real, "utf8"), format: format ?? EXTENSIONS[ext], path: real };
|
|
203
|
+
}
|
|
204
|
+
function withoutSource(prompt, html, format, path) {
|
|
205
|
+
return prompt.replace(`\`\`\`${format}
|
|
206
|
+
${html}
|
|
207
|
+
\`\`\``, () => `The source is in \`${path}\`. Read and edit it there.`);
|
|
208
|
+
}
|
|
209
|
+
|
|
170
210
|
// src/hosted.ts
|
|
171
211
|
async function apiRequest(path, options) {
|
|
172
212
|
const { apiKey, apiUrl, method = "GET", body, timeoutMs = 3e4 } = options;
|
|
@@ -255,8 +295,10 @@ function worstScore(scores) {
|
|
|
255
295
|
}
|
|
256
296
|
var formatEnum = z.enum(["html", "jsx", "mjml", "maizzle"]).optional();
|
|
257
297
|
var detailEnum = z.enum(["summary", "full"]).optional().describe(
|
|
258
|
-
"'summary' (default) returns one entry per
|
|
298
|
+
"'summary' (default) returns one entry per error or warning, listing the clients it affects, and only counts info findings (safe fallbacks). 'full' returns one entry per client with fix snippets, info included."
|
|
259
299
|
);
|
|
300
|
+
var htmlParam = z.string().optional().describe("The email source: HTML, or an MJML / Maizzle / React Email template when `format` says so. Pass this or `path`.");
|
|
301
|
+
var pathParam = z.string().optional().describe("Absolute path to the email source file (.html, .htm, .mjml, .jsx, .tsx, .vue), instead of `html`. Prefer it when the email is on disk: the source stays out of your context. The extension sets `format` unless you pass one.");
|
|
260
302
|
var targetingPolicyEnum = z.enum(["progressive", "strict", "lenient"]).optional().describe(
|
|
261
303
|
"'progressive' (default) suppresses compatibility false-positives inside client-targeted scopes and only flags deprecated or dangerous hacks. 'strict' treats targeting as non-standard and reports every hack. 'lenient' suppresses false positives and ignores non-fatal targeting warnings."
|
|
262
304
|
);
|
|
@@ -307,11 +349,14 @@ function compatibilityPayload(warnings, detail, clients) {
|
|
|
307
349
|
}))
|
|
308
350
|
};
|
|
309
351
|
}
|
|
310
|
-
const
|
|
352
|
+
const collapsed = collapseWarnings(scoped);
|
|
353
|
+
const findings = collapsed.filter((f) => f.severity !== "info");
|
|
354
|
+
const info = collapsed.length - findings.length;
|
|
311
355
|
return {
|
|
312
356
|
warningCount: scoped.length,
|
|
313
357
|
findingCount: findings.length,
|
|
314
|
-
findings
|
|
358
|
+
findings,
|
|
359
|
+
...info ? { infoFindings: info, infoNote: "Safe fallbacks, not problems. Pass detail:'full' to list them." } : {}
|
|
315
360
|
};
|
|
316
361
|
}
|
|
317
362
|
function positionsApply(format) {
|
|
@@ -328,14 +373,14 @@ function withPositions(w) {
|
|
|
328
373
|
};
|
|
329
374
|
}
|
|
330
375
|
function validateHtmlSize(html) {
|
|
331
|
-
if (html.length >
|
|
332
|
-
return mcpError(`HTML input exceeds ${Math.round(
|
|
376
|
+
if (html.length > MAX_HTML_SIZE2) {
|
|
377
|
+
return mcpError(`HTML input exceeds ${Math.round(MAX_HTML_SIZE2 / 1024)}KB limit. Reduce the email size and try again.`);
|
|
333
378
|
}
|
|
334
379
|
return null;
|
|
335
380
|
}
|
|
336
381
|
var server = new McpServer({
|
|
337
382
|
name: "emailens",
|
|
338
|
-
version: "0.
|
|
383
|
+
version: "0.13.1"
|
|
339
384
|
});
|
|
340
385
|
server.registerTool(
|
|
341
386
|
"preview_email",
|
|
@@ -343,7 +388,8 @@ server.registerTool(
|
|
|
343
388
|
title: "Preview Email",
|
|
344
389
|
description: `Full email compatibility preview, transforms HTML for ${EMAIL_CLIENTS2.length} email clients (Gmail, Outlook, Apple Mail, Yahoo, Samsung, Thunderbird, HEY, Proton Mail, AOL, Fastmail, Superhuman), analyzes CSS, generates scores, simulates dark mode, checks inbox preview and email size.`,
|
|
345
390
|
inputSchema: {
|
|
346
|
-
html:
|
|
391
|
+
html: htmlParam,
|
|
392
|
+
path: pathParam,
|
|
347
393
|
clients: z.array(z.string()).optional().describe("Optional client ID filter (e.g. ['gmail-web', 'outlook-windows'])"),
|
|
348
394
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
|
|
349
395
|
targetingPolicy: targetingPolicyEnum
|
|
@@ -356,7 +402,10 @@ server.registerTool(
|
|
|
356
402
|
openWorldHint: false
|
|
357
403
|
}
|
|
358
404
|
},
|
|
359
|
-
async ({ html, clients, format, targetingPolicy }) => {
|
|
405
|
+
async ({ html: inline, path, clients, format: given, targetingPolicy }) => {
|
|
406
|
+
const input = await readSource(inline, path, given);
|
|
407
|
+
if (!input.ok) return mcpError(input.message);
|
|
408
|
+
const { html, format } = input;
|
|
360
409
|
const sizeError = validateHtmlSize(html);
|
|
361
410
|
if (sizeError) return sizeError;
|
|
362
411
|
const source = await toHtml(html, format);
|
|
@@ -369,7 +418,7 @@ server.registerTool(
|
|
|
369
418
|
const filter = clients.filter((c) => validClientIds.has(c));
|
|
370
419
|
if (filter.length === 0) {
|
|
371
420
|
return mcpError(
|
|
372
|
-
JSON.stringify({ error: "No valid client IDs provided", validClientIds: Array.from(validClientIds) }
|
|
421
|
+
JSON.stringify({ error: "No valid client IDs provided", validClientIds: Array.from(validClientIds) })
|
|
373
422
|
);
|
|
374
423
|
}
|
|
375
424
|
transforms = filter.map((c) => transformForClient(source.html, c, framework));
|
|
@@ -413,7 +462,7 @@ server.registerTool(
|
|
|
413
462
|
if (!config.isHosted) {
|
|
414
463
|
result.tip = overallScore < 90 ? "Capture real screenshots to verify these issues across clients. Add EMAILENS_API_KEY to your MCP config \u2192 app.emailens.dev/dashboard/api?utm_source=mcp" : "Checked by Emailens against 21 email clients. Confirm on real Outlook, Gmail and Apple Mail \u2192 emailens.dev/real-devices?utm_source=mcp";
|
|
415
464
|
}
|
|
416
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
465
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
417
466
|
}
|
|
418
467
|
);
|
|
419
468
|
server.registerTool(
|
|
@@ -422,7 +471,8 @@ server.registerTool(
|
|
|
422
471
|
title: "Analyze Email",
|
|
423
472
|
description: "Fix error and warning findings. Info findings are not problems (the email falls back safely there): never remove responsive (@media) or dark-mode CSS to clear them. Quick CSS compatibility analysis, returns per-client scores and one finding per problem, listing the clients it affects. For HTML input each finding carries loc (line, column, offset) and alsoAtLines, so you can edit the exact source. Narrow with clients, or pass detail:'full' for the per-client breakdown with fix snippets. Use audit_email for a full quality report (spam, links, a11y, images, etc.).",
|
|
424
473
|
inputSchema: {
|
|
425
|
-
html:
|
|
474
|
+
html: htmlParam,
|
|
475
|
+
path: pathParam,
|
|
426
476
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
|
|
427
477
|
detail: detailEnum,
|
|
428
478
|
clients: clientsParam,
|
|
@@ -436,7 +486,10 @@ server.registerTool(
|
|
|
436
486
|
openWorldHint: false
|
|
437
487
|
}
|
|
438
488
|
},
|
|
439
|
-
async ({ html, format, detail, clients, targetingPolicy }) => {
|
|
489
|
+
async ({ html: inline, path, format: given, detail, clients, targetingPolicy }) => {
|
|
490
|
+
const input = await readSource(inline, path, given);
|
|
491
|
+
if (!input.ok) return mcpError(input.message);
|
|
492
|
+
const { html, format } = input;
|
|
440
493
|
const sizeError = validateHtmlSize(html);
|
|
441
494
|
if (sizeError) return sizeError;
|
|
442
495
|
const clientError = validateClients(clients);
|
|
@@ -458,9 +511,7 @@ server.registerTool(
|
|
|
458
511
|
overallScore,
|
|
459
512
|
scores,
|
|
460
513
|
...compatibilityPayload(warnings, detail, clients)
|
|
461
|
-
}
|
|
462
|
-
null,
|
|
463
|
-
2
|
|
514
|
+
}
|
|
464
515
|
)
|
|
465
516
|
}
|
|
466
517
|
]
|
|
@@ -473,7 +524,8 @@ server.registerTool(
|
|
|
473
524
|
title: "Audit Email",
|
|
474
525
|
description: "Fix error and warning findings, and restore anything listed in designRemovals. Info findings are not problems (the email falls back safely there): never remove responsive (@media), dark-mode CSS or rounded corners to clear them. designRemovals compares this version with the first one audited for the same email (or with previousHtml): design the edits deleted, such as @media rules, dark mode, corners, gradients, VML fallbacks or images. Comprehensive email quality audit: CSS compatibility, spam scoring, link validation, accessibility, images, inbox preview, size (Gmail clipping), template variables, content overflow, visual fallbacks, dark-mode and mobile text contrast, design consistency, structural faults in Outlook-only VML (which lives inside conditional comments and is invisible to every other check), style survival: CSS a client parses correctly and then discards, such as a stylesheet past Gmail's 16 KB ceiling or every rule after a `}}`, which no support matrix can express, and client targeting: HowToTarget hacks such as Gmail u+.body or MSO conditionals, which do not score as css-hack. For HTML input, findings tied to a specific element carry loc (line, column, offset) so you can edit the exact source. Compatibility is collapsed to one finding per problem listing the clients it affects; pass detail:'full' for the per-client breakdown with fix snippets. Use skip to omit checks, targetingPolicy to change how targeted scopes affect scores, and clients to narrow which clients are reported.",
|
|
475
526
|
inputSchema: {
|
|
476
|
-
html:
|
|
527
|
+
html: htmlParam,
|
|
528
|
+
path: pathParam,
|
|
477
529
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
|
|
478
530
|
skip: z.array(skipEnum).optional().describe("Checks to skip (e.g. ['spam', 'images'])"),
|
|
479
531
|
detail: detailEnum,
|
|
@@ -490,7 +542,10 @@ server.registerTool(
|
|
|
490
542
|
openWorldHint: false
|
|
491
543
|
}
|
|
492
544
|
},
|
|
493
|
-
async ({ html, format, skip, detail, clients, targetingPolicy, emailType, previousHtml }) => {
|
|
545
|
+
async ({ html: inline, path, format: given, skip, detail, clients, targetingPolicy, emailType, previousHtml }) => {
|
|
546
|
+
const input = await readSource(inline, path, given);
|
|
547
|
+
if (!input.ok) return mcpError(input.message);
|
|
548
|
+
const { html, format } = input;
|
|
494
549
|
const sizeError = validateHtmlSize(html);
|
|
495
550
|
if (sizeError) return sizeError;
|
|
496
551
|
const clientError = validateClients(clients);
|
|
@@ -533,9 +588,7 @@ server.registerTool(
|
|
|
533
588
|
design: report.design,
|
|
534
589
|
styleSurvival: report.styleSurvival,
|
|
535
590
|
targeting: report.targeting
|
|
536
|
-
}
|
|
537
|
-
null,
|
|
538
|
-
2
|
|
591
|
+
}
|
|
539
592
|
)
|
|
540
593
|
}
|
|
541
594
|
]
|
|
@@ -548,7 +601,8 @@ server.registerTool(
|
|
|
548
601
|
title: "Fix Email",
|
|
549
602
|
description: "Generate a structured fix prompt for email compatibility issues. Returns markdown with the original code, detected issues (CSS or structural), fix snippets, and format-specific instructions. Use after preview_email or analyze_email.",
|
|
550
603
|
inputSchema: {
|
|
551
|
-
html:
|
|
604
|
+
html: htmlParam,
|
|
605
|
+
path: pathParam,
|
|
552
606
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle."),
|
|
553
607
|
scope: z.enum(["all", "current"]).optional().describe("'all' (default) or 'current' (requires selectedClientId)"),
|
|
554
608
|
selectedClientId: z.string().optional().describe("Client ID to scope fixes to (e.g. 'outlook-windows')")
|
|
@@ -561,7 +615,10 @@ server.registerTool(
|
|
|
561
615
|
openWorldHint: false
|
|
562
616
|
}
|
|
563
617
|
},
|
|
564
|
-
async ({ html, format, scope, selectedClientId }) => {
|
|
618
|
+
async ({ html: inline, path, format: given, scope, selectedClientId }) => {
|
|
619
|
+
const input = await readSource(inline, path, given);
|
|
620
|
+
if (!input.ok) return mcpError(input.message);
|
|
621
|
+
const { html, format } = input;
|
|
565
622
|
const sizeError = validateHtmlSize(html);
|
|
566
623
|
if (sizeError) return sizeError;
|
|
567
624
|
const source = await toHtml(html, format);
|
|
@@ -577,7 +634,7 @@ server.registerTool(
|
|
|
577
634
|
content: [
|
|
578
635
|
{
|
|
579
636
|
type: "text",
|
|
580
|
-
text: JSON.stringify({ message: "No compatibility issues found \u2014 nothing to fix.", overallScore: 100 }
|
|
637
|
+
text: JSON.stringify({ message: "No compatibility issues found \u2014 nothing to fix.", overallScore: 100 })
|
|
581
638
|
}
|
|
582
639
|
]
|
|
583
640
|
};
|
|
@@ -614,12 +671,10 @@ server.registerTool(
|
|
|
614
671
|
truncated: estimate.truncated
|
|
615
672
|
},
|
|
616
673
|
note: "Apply the fixes in the prompt below. Structural issues require HTML restructuring (tables, VML conditionals), not just CSS changes."
|
|
617
|
-
}
|
|
618
|
-
null,
|
|
619
|
-
2
|
|
674
|
+
}
|
|
620
675
|
)
|
|
621
676
|
},
|
|
622
|
-
{ type: "text", text: prompt }
|
|
677
|
+
{ type: "text", text: input.path ? withoutSource(prompt, html, inputFormat, input.path) : prompt }
|
|
623
678
|
]
|
|
624
679
|
};
|
|
625
680
|
}
|
|
@@ -650,9 +705,7 @@ server.registerTool(
|
|
|
650
705
|
engine: c.engine,
|
|
651
706
|
darkModeSupport: c.darkModeSupport,
|
|
652
707
|
...c.deprecated ? { deprecated: c.deprecated } : {}
|
|
653
|
-
}))
|
|
654
|
-
null,
|
|
655
|
-
2
|
|
708
|
+
}))
|
|
656
709
|
)
|
|
657
710
|
}
|
|
658
711
|
]
|
|
@@ -665,8 +718,10 @@ server.registerTool(
|
|
|
665
718
|
title: "Diff Emails",
|
|
666
719
|
description: "Compare two email HTML versions, shows score changes, fixed issues, and newly introduced issues per client. Use after making fixes to verify improvements.",
|
|
667
720
|
inputSchema: {
|
|
668
|
-
before: z.string().describe("Original email source"),
|
|
669
|
-
after: z.string().describe("Modified email source, in the same format"),
|
|
721
|
+
before: z.string().optional().describe("Original email source. Pass this or `beforePath`."),
|
|
722
|
+
after: z.string().optional().describe("Modified email source, in the same format. Pass this or `afterPath`."),
|
|
723
|
+
beforePath: z.string().optional().describe("Absolute path to the original source file, instead of `before`"),
|
|
724
|
+
afterPath: z.string().optional().describe("Absolute path to the modified source file, instead of `after`"),
|
|
670
725
|
format: formatEnum.describe("Source format: 'html' (default), 'jsx' (React Email), 'mjml', or 'maizzle' (HTML template or Vue single-file component). Anything but 'html' is compiled before analysis and sets the syntax of the fix snippets. A pasted Vue file with no format is compiled as maizzle.")
|
|
671
726
|
},
|
|
672
727
|
annotations: {
|
|
@@ -677,7 +732,14 @@ server.registerTool(
|
|
|
677
732
|
openWorldHint: false
|
|
678
733
|
}
|
|
679
734
|
},
|
|
680
|
-
async ({ before, after, format }) => {
|
|
735
|
+
async ({ before: beforeInline, after: afterInline, beforePath, afterPath, format: given }) => {
|
|
736
|
+
const beforeInput = await readSource(beforeInline, beforePath, given, { html: "before", path: "beforePath" });
|
|
737
|
+
if (!beforeInput.ok) return mcpError(beforeInput.message);
|
|
738
|
+
const afterInput = await readSource(afterInline, afterPath, given, { html: "after", path: "afterPath" });
|
|
739
|
+
if (!afterInput.ok) return mcpError(afterInput.message);
|
|
740
|
+
const before = beforeInput.html;
|
|
741
|
+
const after = afterInput.html;
|
|
742
|
+
const format = afterInput.format ?? beforeInput.format;
|
|
681
743
|
const beforeSizeError = validateHtmlSize(before);
|
|
682
744
|
if (beforeSizeError) return beforeSizeError;
|
|
683
745
|
const afterSizeError = validateHtmlSize(after);
|
|
@@ -729,9 +791,7 @@ server.registerTool(
|
|
|
729
791
|
clientsUnchanged,
|
|
730
792
|
avgScoreDelta: results.length > 0 ? Math.round(totalDelta / results.length * 10) / 10 : 0
|
|
731
793
|
}
|
|
732
|
-
}
|
|
733
|
-
null,
|
|
734
|
-
2
|
|
794
|
+
}
|
|
735
795
|
)
|
|
736
796
|
}
|
|
737
797
|
]
|
|
@@ -762,7 +822,7 @@ server.registerTool(
|
|
|
762
822
|
}
|
|
763
823
|
const report = await _checkDeliverability(domain);
|
|
764
824
|
return {
|
|
765
|
-
content: [{ type: "text", text: JSON.stringify(report
|
|
825
|
+
content: [{ type: "text", text: JSON.stringify(report) }]
|
|
766
826
|
};
|
|
767
827
|
} catch (err) {
|
|
768
828
|
const msg = err.message ?? String(err);
|
|
@@ -781,7 +841,8 @@ server.registerTool(
|
|
|
781
841
|
title: "Capture Screenshots",
|
|
782
842
|
description: "Capture real email screenshots across 21 clients (Gmail, Outlook, Apple Mail, etc.) with light and dark mode variants. Screenshots are rendered in real browsers and hosted on CDN. Requires EMAILENS_API_KEY env var \u2014 free plan at emailens.dev?utm_source=mcp.",
|
|
783
843
|
inputSchema: {
|
|
784
|
-
html:
|
|
844
|
+
html: htmlParam,
|
|
845
|
+
path: pathParam,
|
|
785
846
|
format: formatEnum.describe("Input format"),
|
|
786
847
|
clients: z.array(z.string()).optional().describe("Filter to specific client IDs"),
|
|
787
848
|
modes: z.array(z.enum(["light", "dark"])).optional().describe("Screenshot modes (default: ['light'])"),
|
|
@@ -795,7 +856,10 @@ server.registerTool(
|
|
|
795
856
|
openWorldHint: true
|
|
796
857
|
}
|
|
797
858
|
},
|
|
798
|
-
async ({ html, format, clients, modes, title: title2 }) => {
|
|
859
|
+
async ({ html: inline, path, format: given, clients, modes, title: title2 }) => {
|
|
860
|
+
const input = await readSource(inline, path, given);
|
|
861
|
+
if (!input.ok) return mcpError(input.message);
|
|
862
|
+
const { html, format } = input;
|
|
799
863
|
if (!config.isHosted) {
|
|
800
864
|
return noApiKeyError("capture_screenshots");
|
|
801
865
|
}
|
|
@@ -836,9 +900,7 @@ server.registerTool(
|
|
|
836
900
|
captured: [],
|
|
837
901
|
failed: [],
|
|
838
902
|
note: `Preview created but screenshot capture failed: ${screenshotResult.error}. View the full preview at the URL above.`
|
|
839
|
-
}
|
|
840
|
-
null,
|
|
841
|
-
2
|
|
903
|
+
}
|
|
842
904
|
)
|
|
843
905
|
}
|
|
844
906
|
]
|
|
@@ -857,7 +919,7 @@ server.registerTool(
|
|
|
857
919
|
if (ss.failed?.length > 0) {
|
|
858
920
|
result.note = `${ss.failed.length} of ${(ss.captured?.length ?? 0) + ss.failed.length} screenshots failed: ${ss.failed.join(", ")}`;
|
|
859
921
|
}
|
|
860
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
922
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
861
923
|
}
|
|
862
924
|
);
|
|
863
925
|
server.registerTool(
|
|
@@ -866,7 +928,8 @@ server.registerTool(
|
|
|
866
928
|
title: "Share Preview",
|
|
867
929
|
description: "Create a shareable link for an email preview. Recipients see the full analysis without needing an account. Requires EMAILENS_API_KEY env var and Dev plan ($9/mo).",
|
|
868
930
|
inputSchema: {
|
|
869
|
-
html:
|
|
931
|
+
html: htmlParam,
|
|
932
|
+
path: pathParam,
|
|
870
933
|
title: z.string().optional().describe("Display title for the share page"),
|
|
871
934
|
format: formatEnum.describe("Input format")
|
|
872
935
|
},
|
|
@@ -878,7 +941,10 @@ server.registerTool(
|
|
|
878
941
|
openWorldHint: true
|
|
879
942
|
}
|
|
880
943
|
},
|
|
881
|
-
async ({ html, title: title2, format }) => {
|
|
944
|
+
async ({ html: inline, path, title: title2, format: given }) => {
|
|
945
|
+
const input = await readSource(inline, path, given);
|
|
946
|
+
if (!input.ok) return mcpError(input.message);
|
|
947
|
+
const { html, format } = input;
|
|
882
948
|
if (!config.isHosted) {
|
|
883
949
|
return noApiKeyError("share_preview", "Shareable links require the Dev plan ($9/mo).");
|
|
884
950
|
}
|
|
@@ -898,9 +964,7 @@ server.registerTool(
|
|
|
898
964
|
{
|
|
899
965
|
shareUrl: `${config.apiUrl}/share/${result.data.id}`,
|
|
900
966
|
expiresAt: result.data.expiresAt
|
|
901
|
-
}
|
|
902
|
-
null,
|
|
903
|
-
2
|
|
967
|
+
}
|
|
904
968
|
)
|
|
905
969
|
}
|
|
906
970
|
]
|
|
@@ -938,7 +1002,7 @@ server.registerTool(
|
|
|
938
1002
|
content: [
|
|
939
1003
|
{
|
|
940
1004
|
type: "text",
|
|
941
|
-
text: JSON.stringify(result.data
|
|
1005
|
+
text: JSON.stringify(result.data)
|
|
942
1006
|
}
|
|
943
1007
|
]
|
|
944
1008
|
};
|
|
@@ -984,7 +1048,7 @@ server.registerTool(
|
|
|
984
1048
|
content: [
|
|
985
1049
|
{
|
|
986
1050
|
type: "text",
|
|
987
|
-
text: JSON.stringify(result.data?.message ?? result.data
|
|
1051
|
+
text: JSON.stringify(result.data?.message ?? result.data)
|
|
988
1052
|
}
|
|
989
1053
|
]
|
|
990
1054
|
};
|
|
@@ -1022,7 +1086,7 @@ server.registerTool(
|
|
|
1022
1086
|
content: [
|
|
1023
1087
|
{
|
|
1024
1088
|
type: "text",
|
|
1025
|
-
text: JSON.stringify(result.data
|
|
1089
|
+
text: JSON.stringify(result.data)
|
|
1026
1090
|
}
|
|
1027
1091
|
]
|
|
1028
1092
|
};
|
|
@@ -1059,7 +1123,7 @@ server.registerTool(
|
|
|
1059
1123
|
content: [
|
|
1060
1124
|
{
|
|
1061
1125
|
type: "text",
|
|
1062
|
-
text: JSON.stringify(result.data
|
|
1126
|
+
text: JSON.stringify(result.data)
|
|
1063
1127
|
}
|
|
1064
1128
|
]
|
|
1065
1129
|
};
|