frameshot-mcp 0.2.0 → 0.6.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 +93 -128
- package/action.yml +130 -0
- package/dist/chunk-47YJG5HR.js +690 -0
- package/dist/index.js +1091 -214
- package/dist/renderer.d.ts +241 -0
- package/dist/renderer.js +28 -0
- package/package.json +28 -5
- package/scripts/render-changed.mjs +90 -0
- package/scripts/setup-labels.sh +48 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import {
|
|
3
|
+
AuditUseCase,
|
|
4
|
+
BrowserPool,
|
|
5
|
+
CatalogUseCase,
|
|
6
|
+
DEVICE_PRESETS,
|
|
7
|
+
DiffUseCase,
|
|
8
|
+
EXT_TO_FRAMEWORK,
|
|
9
|
+
HtmlBuilder,
|
|
10
|
+
ImageComparator,
|
|
11
|
+
RenderUseCase,
|
|
12
|
+
ScreenshotUseCase,
|
|
13
|
+
SnapshotStore,
|
|
14
|
+
SnapshotUseCase,
|
|
15
|
+
__export
|
|
16
|
+
} from "./chunk-47YJG5HR.js";
|
|
7
17
|
|
|
8
18
|
// src/index.ts
|
|
19
|
+
import { execSync } from "child_process";
|
|
9
20
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
22
|
|
|
@@ -14523,232 +14534,1098 @@ function date4(params) {
|
|
|
14523
14534
|
// node_modules/zod/v4/classic/external.js
|
|
14524
14535
|
config(en_default());
|
|
14525
14536
|
|
|
14526
|
-
// src/
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
|
|
14537
|
+
// src/tools/audit-tools.ts
|
|
14538
|
+
function registerAuditTools(server2, useCase) {
|
|
14539
|
+
server2.tool(
|
|
14540
|
+
"audit_a11y",
|
|
14541
|
+
"Run an accessibility audit (axe-core) on a rendered component. Returns WCAG violations with impact level, description, and affected HTML nodes. Use this to catch a11y issues before shipping.",
|
|
14542
|
+
{
|
|
14543
|
+
code: external_exports.string().describe("Component code to audit"),
|
|
14544
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
14545
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14546
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
14547
|
+
},
|
|
14548
|
+
async ({ code, framework, width, height }) => {
|
|
14549
|
+
try {
|
|
14550
|
+
const result = await useCase.auditA11y(code, framework, {
|
|
14551
|
+
viewport: { width, height }
|
|
14552
|
+
});
|
|
14553
|
+
if (result.violations.length === 0) {
|
|
14554
|
+
return {
|
|
14555
|
+
content: [
|
|
14556
|
+
{
|
|
14557
|
+
type: "text",
|
|
14558
|
+
text: `\u2705 No accessibility violations found. (${result.passes} rules passed, ${result.incomplete} need review)`
|
|
14559
|
+
}
|
|
14560
|
+
]
|
|
14561
|
+
};
|
|
14562
|
+
}
|
|
14563
|
+
const report = result.violations.map((v) => {
|
|
14564
|
+
const nodes = v.nodes.slice(0, 3).map((n) => ` ${n.target.join(" > ")}
|
|
14565
|
+
${n.html}`).join("\n");
|
|
14566
|
+
return `[${v.impact?.toUpperCase()}] ${v.id}
|
|
14567
|
+
${v.description}
|
|
14568
|
+
${v.helpUrl}
|
|
14569
|
+
${nodes}`;
|
|
14570
|
+
}).join("\n\n");
|
|
14571
|
+
return {
|
|
14572
|
+
content: [
|
|
14573
|
+
{
|
|
14574
|
+
type: "text",
|
|
14575
|
+
text: `Found ${result.violations.length} accessibility violation(s):
|
|
14576
|
+
|
|
14577
|
+
${report}
|
|
14578
|
+
|
|
14579
|
+
(${result.passes} rules passed, ${result.incomplete} need review)`
|
|
14580
|
+
}
|
|
14581
|
+
]
|
|
14582
|
+
};
|
|
14583
|
+
} catch (error51) {
|
|
14584
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14585
|
+
return {
|
|
14586
|
+
content: [
|
|
14587
|
+
{ type: "text", text: `A11y audit failed: ${msg}` }
|
|
14588
|
+
],
|
|
14589
|
+
isError: true
|
|
14590
|
+
};
|
|
14591
|
+
}
|
|
14592
|
+
}
|
|
14556
14593
|
);
|
|
14557
|
-
|
|
14558
|
-
|
|
14559
|
-
|
|
14560
|
-
|
|
14561
|
-
|
|
14562
|
-
|
|
14563
|
-
|
|
14564
|
-
|
|
14565
|
-
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
${
|
|
14574
|
-
|
|
14575
|
-
${
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14583
|
-
|
|
14584
|
-
|
|
14585
|
-
|
|
14586
|
-
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
|
|
14591
|
-
|
|
14592
|
-
}
|
|
14593
|
-
async function renderSingle(engine, html, options) {
|
|
14594
|
-
const {
|
|
14595
|
-
width = 1280,
|
|
14596
|
-
height = 800,
|
|
14597
|
-
fullPage = true,
|
|
14598
|
-
waitFor = 0
|
|
14599
|
-
} = options;
|
|
14600
|
-
const slot = await getSlot(engine);
|
|
14601
|
-
const { page } = slot;
|
|
14602
|
-
const currentViewport = page.viewportSize();
|
|
14603
|
-
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14604
|
-
await page.setViewportSize({ width, height });
|
|
14605
|
-
}
|
|
14606
|
-
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
14607
|
-
if (waitFor > 0) {
|
|
14608
|
-
await page.waitForTimeout(waitFor);
|
|
14609
|
-
}
|
|
14610
|
-
const screenshot = await page.screenshot({ type: "png", fullPage });
|
|
14611
|
-
const metrics = await page.evaluate(() => ({
|
|
14612
|
-
w: document.documentElement.scrollWidth,
|
|
14613
|
-
h: document.documentElement.scrollHeight
|
|
14614
|
-
}));
|
|
14615
|
-
return {
|
|
14616
|
-
engine,
|
|
14617
|
-
image: screenshot.toString("base64"),
|
|
14618
|
-
width: metrics.w,
|
|
14619
|
-
height: metrics.h
|
|
14620
|
-
};
|
|
14621
|
-
}
|
|
14622
|
-
async function render(code, framework, options = {}) {
|
|
14623
|
-
const engines = options.engines ?? ["chromium"];
|
|
14624
|
-
const html = wrapComponent(code, framework);
|
|
14625
|
-
const results = await Promise.all(
|
|
14626
|
-
engines.map((e) => renderSingle(e, html, options))
|
|
14594
|
+
server2.tool(
|
|
14595
|
+
"perf_audit",
|
|
14596
|
+
"Analyze component performance: DOM element count, tree depth, render time, script/stylesheet count. Use this to catch bloated components before they ship.",
|
|
14597
|
+
{
|
|
14598
|
+
code: external_exports.string().describe("Component code to audit"),
|
|
14599
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
|
|
14600
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14601
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
14602
|
+
},
|
|
14603
|
+
async ({ code, framework, width, height }) => {
|
|
14604
|
+
try {
|
|
14605
|
+
const metrics = await useCase.perfAudit(code, framework, {
|
|
14606
|
+
viewport: { width, height }
|
|
14607
|
+
});
|
|
14608
|
+
const lines = [
|
|
14609
|
+
`\u23F1\uFE0F Render time: ${metrics.renderTimeMs}ms`,
|
|
14610
|
+
`\u{1F4E6} DOM elements: ${metrics.domElements}`,
|
|
14611
|
+
`\u{1F333} DOM depth: ${metrics.domDepth}`,
|
|
14612
|
+
`\u{1F4DC} Scripts: ${metrics.scriptCount}`,
|
|
14613
|
+
`\u{1F3A8} Stylesheets: ${metrics.styleSheetCount}`,
|
|
14614
|
+
`\u{1F5BC}\uFE0F Images: ${metrics.imageCount}`,
|
|
14615
|
+
`\u{1F4D0} Total DOM size: ${(metrics.totalDomSize / 1024).toFixed(1)}KB`
|
|
14616
|
+
];
|
|
14617
|
+
return {
|
|
14618
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
14619
|
+
};
|
|
14620
|
+
} catch (error51) {
|
|
14621
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14622
|
+
return {
|
|
14623
|
+
content: [
|
|
14624
|
+
{ type: "text", text: `Perf audit failed: ${msg}` }
|
|
14625
|
+
],
|
|
14626
|
+
isError: true
|
|
14627
|
+
};
|
|
14628
|
+
}
|
|
14629
|
+
}
|
|
14627
14630
|
);
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
|
|
14640
|
-
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
|
|
14651
|
-
|
|
14652
|
-
|
|
14653
|
-
|
|
14631
|
+
}
|
|
14632
|
+
|
|
14633
|
+
// src/tools/catalog-tools.ts
|
|
14634
|
+
function registerCatalogTools(server2, useCase) {
|
|
14635
|
+
server2.tool(
|
|
14636
|
+
"render_catalog",
|
|
14637
|
+
"Scan a directory for component files (.jsx/.tsx/.vue/.svelte/.html) and render a screenshot of each. Returns a visual catalog \u2014 like Storybook but zero-config. Use this to quickly audit all components in a folder.",
|
|
14638
|
+
{
|
|
14639
|
+
directory: external_exports.string().describe("Absolute path to the directory to scan"),
|
|
14640
|
+
recursive: external_exports.boolean().optional().default(false).describe("Scan subdirectories recursively"),
|
|
14641
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14642
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
14643
|
+
darkMode: external_exports.boolean().optional().default(false).describe("Render with dark mode"),
|
|
14644
|
+
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
|
|
14645
|
+
},
|
|
14646
|
+
async ({
|
|
14647
|
+
directory,
|
|
14648
|
+
recursive,
|
|
14649
|
+
width,
|
|
14650
|
+
height,
|
|
14651
|
+
darkMode,
|
|
14652
|
+
tailwindVersion
|
|
14653
|
+
}) => {
|
|
14654
|
+
try {
|
|
14655
|
+
const results = await useCase.renderCatalog(directory, {
|
|
14656
|
+
recursive,
|
|
14657
|
+
viewport: { width, height },
|
|
14658
|
+
darkMode,
|
|
14659
|
+
tailwindVersion
|
|
14660
|
+
});
|
|
14661
|
+
if (results.length === 0) {
|
|
14662
|
+
return {
|
|
14663
|
+
content: [
|
|
14664
|
+
{
|
|
14665
|
+
type: "text",
|
|
14666
|
+
text: `No component files found in ${directory}`
|
|
14667
|
+
}
|
|
14668
|
+
]
|
|
14669
|
+
};
|
|
14670
|
+
}
|
|
14671
|
+
const content = results.flatMap((r) => {
|
|
14672
|
+
if (!r.image) {
|
|
14673
|
+
return [
|
|
14674
|
+
{
|
|
14675
|
+
type: "text",
|
|
14676
|
+
text: `\u274C ${r.path} \u2014 failed to render`
|
|
14677
|
+
}
|
|
14678
|
+
];
|
|
14679
|
+
}
|
|
14680
|
+
return [
|
|
14681
|
+
{
|
|
14682
|
+
type: "image",
|
|
14683
|
+
data: r.image,
|
|
14684
|
+
mimeType: "image/png"
|
|
14685
|
+
},
|
|
14686
|
+
{
|
|
14687
|
+
type: "text",
|
|
14688
|
+
text: `[${r.framework}] ${r.path} \u2014 ${r.width}x${r.height}${r.consoleErrors.length ? `
|
|
14689
|
+
\u26A0\uFE0F ${r.consoleErrors.join("\n")}` : ""}`
|
|
14690
|
+
}
|
|
14691
|
+
];
|
|
14692
|
+
});
|
|
14693
|
+
content.unshift({
|
|
14694
|
+
type: "text",
|
|
14695
|
+
text: `\u{1F4E6} Component catalog: ${results.length} files from ${directory}`
|
|
14696
|
+
});
|
|
14697
|
+
return { content };
|
|
14698
|
+
} catch (error51) {
|
|
14699
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14700
|
+
return {
|
|
14701
|
+
content: [
|
|
14702
|
+
{ type: "text", text: `Catalog render failed: ${msg}` }
|
|
14703
|
+
],
|
|
14704
|
+
isError: true
|
|
14705
|
+
};
|
|
14706
|
+
}
|
|
14707
|
+
}
|
|
14654
14708
|
);
|
|
14655
|
-
return results;
|
|
14656
14709
|
}
|
|
14657
|
-
|
|
14658
|
-
|
|
14659
|
-
|
|
14660
|
-
|
|
14661
|
-
|
|
14662
|
-
|
|
14710
|
+
|
|
14711
|
+
// src/tools/diff-tools.ts
|
|
14712
|
+
function registerDiffTools(server2, useCase) {
|
|
14713
|
+
server2.tool(
|
|
14714
|
+
"diff_component",
|
|
14715
|
+
"Visual regression test: render before/after code and return a pixel diff image with percentage changed. Use this during refactoring to catch unintended visual changes.",
|
|
14716
|
+
{
|
|
14717
|
+
before: external_exports.string().describe("Component code BEFORE the change"),
|
|
14718
|
+
after: external_exports.string().describe("Component code AFTER the change"),
|
|
14719
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
14720
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14721
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
14722
|
+
},
|
|
14723
|
+
async ({ before, after, framework, width, height }) => {
|
|
14724
|
+
try {
|
|
14725
|
+
const result = await useCase.diffComponent(before, after, framework, {
|
|
14726
|
+
viewport: { width, height }
|
|
14727
|
+
});
|
|
14728
|
+
const content = [
|
|
14729
|
+
{
|
|
14730
|
+
type: "text",
|
|
14731
|
+
text: `Visual diff: ${result.diffPercentage}% pixels changed (${result.diffPixels}/${result.totalPixels})`
|
|
14732
|
+
},
|
|
14733
|
+
{ type: "text", text: "Before:" },
|
|
14734
|
+
{
|
|
14735
|
+
type: "image",
|
|
14736
|
+
data: result.before,
|
|
14737
|
+
mimeType: "image/png"
|
|
14738
|
+
},
|
|
14739
|
+
{ type: "text", text: "After:" },
|
|
14740
|
+
{
|
|
14741
|
+
type: "image",
|
|
14742
|
+
data: result.after,
|
|
14743
|
+
mimeType: "image/png"
|
|
14744
|
+
},
|
|
14745
|
+
{ type: "text", text: "Diff (red = changed pixels):" },
|
|
14746
|
+
{
|
|
14747
|
+
type: "image",
|
|
14748
|
+
data: result.diff,
|
|
14749
|
+
mimeType: "image/png"
|
|
14750
|
+
}
|
|
14751
|
+
];
|
|
14752
|
+
return { content };
|
|
14753
|
+
} catch (error51) {
|
|
14754
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14755
|
+
return {
|
|
14756
|
+
content: [{ type: "text", text: `Diff failed: ${msg}` }],
|
|
14757
|
+
isError: true
|
|
14758
|
+
};
|
|
14759
|
+
}
|
|
14760
|
+
}
|
|
14761
|
+
);
|
|
14762
|
+
server2.tool(
|
|
14763
|
+
"diff_reference",
|
|
14764
|
+
"Compare rendered component output against a reference image (e.g. from Figma export or previous snapshot). Returns pixel diff percentage and pass/fail status. Use this for design QA \u2014 verify your component matches the design.",
|
|
14765
|
+
{
|
|
14766
|
+
code: external_exports.string().describe("Component code to render and compare"),
|
|
14767
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
14768
|
+
referenceImage: external_exports.string().describe("Reference image as base64-encoded PNG"),
|
|
14769
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14770
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
14771
|
+
threshold: external_exports.number().optional().default(0.1).describe(
|
|
14772
|
+
"Pixel matching threshold (0-1). Lower = stricter. Default 0.1."
|
|
14773
|
+
),
|
|
14774
|
+
darkMode: external_exports.boolean().optional().default(false).describe("Render with dark mode"),
|
|
14775
|
+
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
|
|
14776
|
+
},
|
|
14777
|
+
async ({
|
|
14778
|
+
code,
|
|
14779
|
+
framework,
|
|
14780
|
+
referenceImage,
|
|
14781
|
+
width,
|
|
14782
|
+
height,
|
|
14783
|
+
threshold,
|
|
14784
|
+
darkMode,
|
|
14785
|
+
tailwindVersion
|
|
14786
|
+
}) => {
|
|
14787
|
+
try {
|
|
14788
|
+
const result = await useCase.diffFromReference(
|
|
14789
|
+
code,
|
|
14790
|
+
framework,
|
|
14791
|
+
referenceImage,
|
|
14792
|
+
{
|
|
14793
|
+
viewport: { width, height },
|
|
14794
|
+
threshold,
|
|
14795
|
+
darkMode,
|
|
14796
|
+
tailwindVersion
|
|
14797
|
+
}
|
|
14798
|
+
);
|
|
14799
|
+
const status = result.passed ? "PASS" : "FAIL";
|
|
14800
|
+
const content = [
|
|
14801
|
+
{
|
|
14802
|
+
type: "text",
|
|
14803
|
+
text: `${status}: ${result.diffPercentage}% pixels differ (${result.diffPixels}/${result.totalPixels})`
|
|
14804
|
+
},
|
|
14805
|
+
{ type: "text", text: "Rendered:" },
|
|
14806
|
+
{
|
|
14807
|
+
type: "image",
|
|
14808
|
+
data: result.rendered,
|
|
14809
|
+
mimeType: "image/png"
|
|
14810
|
+
},
|
|
14811
|
+
{ type: "text", text: "Diff (red = changed pixels):" },
|
|
14812
|
+
{
|
|
14813
|
+
type: "image",
|
|
14814
|
+
data: result.diff,
|
|
14815
|
+
mimeType: "image/png"
|
|
14816
|
+
}
|
|
14817
|
+
];
|
|
14818
|
+
return { content };
|
|
14819
|
+
} catch (error51) {
|
|
14820
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14821
|
+
return {
|
|
14822
|
+
content: [
|
|
14823
|
+
{ type: "text", text: `Reference diff failed: ${msg}` }
|
|
14824
|
+
],
|
|
14825
|
+
isError: true
|
|
14826
|
+
};
|
|
14827
|
+
}
|
|
14828
|
+
}
|
|
14829
|
+
);
|
|
14663
14830
|
}
|
|
14664
14831
|
|
|
14665
|
-
// src/
|
|
14666
|
-
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
|
|
14670
|
-
|
|
14671
|
-
|
|
14672
|
-
|
|
14673
|
-
|
|
14674
|
-
|
|
14675
|
-
|
|
14676
|
-
|
|
14677
|
-
|
|
14678
|
-
|
|
14679
|
-
|
|
14680
|
-
|
|
14681
|
-
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14832
|
+
// src/tools/render-tools.ts
|
|
14833
|
+
import { readFileSync } from "fs";
|
|
14834
|
+
import { extname } from "path";
|
|
14835
|
+
function registerRenderTools(server2, useCase) {
|
|
14836
|
+
server2.tool(
|
|
14837
|
+
"render_file",
|
|
14838
|
+
"Read a component file from disk and render it. Auto-detects framework from file extension (.jsx/.tsx=react, .vue=vue, .svelte=svelte, .html=html).",
|
|
14839
|
+
{
|
|
14840
|
+
path: external_exports.string().describe("Absolute path to the component file"),
|
|
14841
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14842
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
14843
|
+
darkMode: external_exports.boolean().optional().default(false).describe("Render with dark mode"),
|
|
14844
|
+
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
|
|
14845
|
+
},
|
|
14846
|
+
async ({ path, width, height, darkMode, tailwindVersion }) => {
|
|
14847
|
+
try {
|
|
14848
|
+
const code = readFileSync(path, "utf-8");
|
|
14849
|
+
const ext = extname(path).toLowerCase();
|
|
14850
|
+
const framework = EXT_TO_FRAMEWORK[ext] ?? "react";
|
|
14851
|
+
const start = performance.now();
|
|
14852
|
+
const results = await useCase.render(code, framework, {
|
|
14853
|
+
viewport: { width, height },
|
|
14854
|
+
fullPage: true,
|
|
14855
|
+
engines: ["chromium"],
|
|
14856
|
+
darkMode,
|
|
14857
|
+
tailwindVersion
|
|
14858
|
+
});
|
|
14859
|
+
const elapsed = Math.round(performance.now() - start);
|
|
14860
|
+
const content = results.flatMap((r) => [
|
|
14861
|
+
{
|
|
14862
|
+
type: "image",
|
|
14863
|
+
data: r.image,
|
|
14864
|
+
mimeType: "image/png"
|
|
14865
|
+
},
|
|
14866
|
+
{
|
|
14867
|
+
type: "text",
|
|
14868
|
+
text: `[${framework}] ${r.width}x${r.height} (${elapsed}ms)${r.consoleErrors.length ? `
|
|
14869
|
+
\u26A0\uFE0F Console errors:
|
|
14870
|
+
${r.consoleErrors.join("\n")}` : ""}`
|
|
14871
|
+
}
|
|
14872
|
+
]);
|
|
14873
|
+
return { content };
|
|
14874
|
+
} catch (error51) {
|
|
14875
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14876
|
+
return {
|
|
14877
|
+
content: [
|
|
14878
|
+
{ type: "text", text: `File render failed: ${msg}` }
|
|
14879
|
+
],
|
|
14880
|
+
isError: true
|
|
14881
|
+
};
|
|
14882
|
+
}
|
|
14883
|
+
}
|
|
14884
|
+
);
|
|
14885
|
+
server2.tool(
|
|
14886
|
+
"render_component",
|
|
14887
|
+
"Instantly render a React/Vue/HTML component and return screenshots across browser engines. Zero setup needed \u2014 just pass your code. Tailwind CSS is built-in. Use this to visually verify UI code without starting a dev server.",
|
|
14888
|
+
{
|
|
14889
|
+
code: external_exports.string().describe("Component code to render"),
|
|
14890
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
14891
|
+
engines: external_exports.array(external_exports.enum(["chromium", "firefox", "webkit"])).optional().default(["chromium"]).describe(
|
|
14892
|
+
'Browser engines to render in. Use ["chromium","firefox","webkit"] for cross-browser check.'
|
|
14893
|
+
),
|
|
14894
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14895
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
14896
|
+
fullPage: external_exports.boolean().optional().default(true).describe("Capture full scroll height"),
|
|
14897
|
+
darkMode: external_exports.boolean().optional().default(false).describe("Render with Tailwind dark mode (adds 'dark' class to html)"),
|
|
14898
|
+
colorSchemes: external_exports.array(external_exports.enum(["light", "dark"])).optional().describe(
|
|
14899
|
+
'Render both: ["light","dark"] returns 2 screenshots for comparison'
|
|
14900
|
+
),
|
|
14901
|
+
css: external_exports.string().optional().describe("Custom CSS to inject (design tokens, variables, etc)"),
|
|
14902
|
+
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
|
|
14903
|
+
},
|
|
14904
|
+
async ({
|
|
14905
|
+
code,
|
|
14906
|
+
framework,
|
|
14907
|
+
engines,
|
|
14908
|
+
width,
|
|
14909
|
+
height,
|
|
14910
|
+
fullPage,
|
|
14911
|
+
darkMode,
|
|
14912
|
+
colorSchemes,
|
|
14913
|
+
css,
|
|
14914
|
+
tailwindVersion
|
|
14915
|
+
}) => {
|
|
14916
|
+
try {
|
|
14917
|
+
const start = performance.now();
|
|
14918
|
+
const schemes = colorSchemes ?? (darkMode ? ["dark"] : ["light"]);
|
|
14919
|
+
const allResults = await Promise.all(
|
|
14920
|
+
schemes.map(
|
|
14921
|
+
(scheme) => useCase.render(code, framework, {
|
|
14922
|
+
viewport: { width, height },
|
|
14923
|
+
fullPage,
|
|
14924
|
+
engines,
|
|
14925
|
+
darkMode: scheme === "dark",
|
|
14926
|
+
css,
|
|
14927
|
+
tailwindVersion
|
|
14928
|
+
})
|
|
14929
|
+
)
|
|
14930
|
+
);
|
|
14931
|
+
const elapsed = Math.round(performance.now() - start);
|
|
14932
|
+
const results = allResults.flat();
|
|
14933
|
+
const content = results.flatMap((r) => [
|
|
14934
|
+
{
|
|
14935
|
+
type: "image",
|
|
14936
|
+
data: r.image,
|
|
14937
|
+
mimeType: "image/png"
|
|
14938
|
+
},
|
|
14939
|
+
{
|
|
14940
|
+
type: "text",
|
|
14941
|
+
text: `[${r.engine}] ${r.width}x${r.height} (${elapsed}ms)${r.consoleErrors.length ? `
|
|
14942
|
+
\u26A0\uFE0F Console errors:
|
|
14943
|
+
${r.consoleErrors.join("\n")}` : ""}`
|
|
14944
|
+
}
|
|
14945
|
+
]);
|
|
14946
|
+
return { content };
|
|
14947
|
+
} catch (error51) {
|
|
14948
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14949
|
+
return {
|
|
14950
|
+
content: [{ type: "text", text: `Render failed: ${msg}` }],
|
|
14951
|
+
isError: true
|
|
14952
|
+
};
|
|
14953
|
+
}
|
|
14954
|
+
}
|
|
14955
|
+
);
|
|
14956
|
+
server2.tool(
|
|
14957
|
+
"render_responsive",
|
|
14958
|
+
"Render a component at mobile, tablet, and desktop sizes in one call. Returns 3 screenshots for responsive verification.",
|
|
14959
|
+
{
|
|
14960
|
+
code: external_exports.string().describe("Component code to render"),
|
|
14961
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
14962
|
+
devices: external_exports.array(external_exports.enum(["mobile", "tablet", "desktop"])).optional().default(["mobile", "tablet", "desktop"]).describe("Device sizes to render")
|
|
14963
|
+
},
|
|
14964
|
+
async ({ code, framework, devices }) => {
|
|
14965
|
+
try {
|
|
14966
|
+
const results = await Promise.all(
|
|
14967
|
+
devices.map(async (device) => {
|
|
14968
|
+
const preset = DEVICE_PRESETS[device];
|
|
14969
|
+
const [result] = await useCase.render(code, framework, {
|
|
14970
|
+
viewport: { width: preset.width, height: preset.height },
|
|
14971
|
+
fullPage: true,
|
|
14972
|
+
engines: ["chromium"]
|
|
14973
|
+
});
|
|
14974
|
+
return { device, ...result };
|
|
14975
|
+
})
|
|
14976
|
+
);
|
|
14977
|
+
const content = results.flatMap((r) => [
|
|
14978
|
+
{
|
|
14979
|
+
type: "image",
|
|
14980
|
+
data: r.image,
|
|
14981
|
+
mimeType: "image/png"
|
|
14982
|
+
},
|
|
14983
|
+
{
|
|
14984
|
+
type: "text",
|
|
14985
|
+
text: `[${r.device}] ${r.width}x${r.height}`
|
|
14986
|
+
}
|
|
14987
|
+
]);
|
|
14988
|
+
return { content };
|
|
14989
|
+
} catch (error51) {
|
|
14990
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14991
|
+
return {
|
|
14992
|
+
content: [
|
|
14993
|
+
{
|
|
14994
|
+
type: "text",
|
|
14995
|
+
text: `Responsive render failed: ${msg}`
|
|
14996
|
+
}
|
|
14997
|
+
],
|
|
14998
|
+
isError: true
|
|
14999
|
+
};
|
|
15000
|
+
}
|
|
15001
|
+
}
|
|
15002
|
+
);
|
|
15003
|
+
server2.tool(
|
|
15004
|
+
"render_variants",
|
|
15005
|
+
"Render multiple variants of a component (different props/states) in one call. Returns a screenshot for each variant. Use this to verify buttons in all states, theme variations, etc.",
|
|
15006
|
+
{
|
|
15007
|
+
code: external_exports.string().describe("Component code (must export a function that accepts props)"),
|
|
15008
|
+
variants: external_exports.array(
|
|
15009
|
+
external_exports.object({
|
|
15010
|
+
label: external_exports.string().describe("Label for this variant (e.g. 'disabled', 'loading')"),
|
|
15011
|
+
props: external_exports.string().describe("Props as JSON string to pass to the component")
|
|
15012
|
+
})
|
|
15013
|
+
).describe("Array of variants to render"),
|
|
15014
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
15015
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15016
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15017
|
+
},
|
|
15018
|
+
async ({ code, variants, framework, width, height }) => {
|
|
15019
|
+
try {
|
|
15020
|
+
const results = await Promise.all(
|
|
15021
|
+
variants.map(async (variant) => {
|
|
15022
|
+
const wrappedCode = framework === "react" ? `${code}
|
|
15023
|
+
const _VARIANT_PROPS = ${variant.props};
|
|
15024
|
+
function _VariantWrapper() { return <App {..._VARIANT_PROPS} />; }` : code;
|
|
15025
|
+
const renderFramework = framework;
|
|
15026
|
+
const overrideCode = framework === "react" ? wrappedCode.replace(
|
|
15027
|
+
/const _C = typeof App/,
|
|
15028
|
+
"const _C = typeof _VariantWrapper!=='undefined'?_VariantWrapper:typeof App"
|
|
15029
|
+
) : wrappedCode;
|
|
15030
|
+
const [result] = await useCase.render(
|
|
15031
|
+
overrideCode,
|
|
15032
|
+
renderFramework,
|
|
15033
|
+
{
|
|
15034
|
+
viewport: { width, height },
|
|
15035
|
+
fullPage: true,
|
|
15036
|
+
engines: ["chromium"]
|
|
15037
|
+
}
|
|
15038
|
+
);
|
|
15039
|
+
return { label: variant.label, ...result };
|
|
15040
|
+
})
|
|
15041
|
+
);
|
|
15042
|
+
const content = results.flatMap((r) => [
|
|
15043
|
+
{
|
|
15044
|
+
type: "image",
|
|
15045
|
+
data: r.image,
|
|
15046
|
+
mimeType: "image/png"
|
|
15047
|
+
},
|
|
15048
|
+
{
|
|
15049
|
+
type: "text",
|
|
15050
|
+
text: `[${r.label}] ${r.width}x${r.height}${r.consoleErrors.length ? `
|
|
15051
|
+
\u26A0\uFE0F Console errors:
|
|
15052
|
+
${r.consoleErrors.join("\n")}` : ""}`
|
|
15053
|
+
}
|
|
15054
|
+
]);
|
|
15055
|
+
return { content };
|
|
15056
|
+
} catch (error51) {
|
|
15057
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15058
|
+
return {
|
|
15059
|
+
content: [
|
|
15060
|
+
{
|
|
15061
|
+
type: "text",
|
|
15062
|
+
text: `Variants render failed: ${msg}`
|
|
15063
|
+
}
|
|
15064
|
+
],
|
|
15065
|
+
isError: true
|
|
15066
|
+
};
|
|
15067
|
+
}
|
|
15068
|
+
}
|
|
15069
|
+
);
|
|
15070
|
+
server2.tool(
|
|
15071
|
+
"render_interaction",
|
|
15072
|
+
"Render a component, simulate user interactions (click, hover, focus, type), then screenshot the result. Use this to verify hover states, dropdowns, modals, form inputs.",
|
|
15073
|
+
{
|
|
15074
|
+
code: external_exports.string().describe("Component code to render"),
|
|
15075
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
|
|
15076
|
+
interactions: external_exports.array(
|
|
15077
|
+
external_exports.object({
|
|
15078
|
+
action: external_exports.enum(["click", "hover", "focus", "type", "wait"]).describe("Interaction type"),
|
|
15079
|
+
selector: external_exports.string().optional().describe("CSS selector for the target element"),
|
|
15080
|
+
value: external_exports.string().optional().describe("Text to type (for 'type' action)"),
|
|
15081
|
+
ms: external_exports.number().optional().describe("Wait duration in ms (for 'wait' action)")
|
|
15082
|
+
})
|
|
15083
|
+
).describe("Sequence of interactions to perform"),
|
|
15084
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15085
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15086
|
+
},
|
|
15087
|
+
async ({ code, framework, interactions, width, height }) => {
|
|
15088
|
+
try {
|
|
15089
|
+
const result = await useCase.renderInteraction(
|
|
15090
|
+
code,
|
|
15091
|
+
framework,
|
|
15092
|
+
interactions,
|
|
15093
|
+
{
|
|
15094
|
+
viewport: { width, height }
|
|
15095
|
+
}
|
|
15096
|
+
);
|
|
15097
|
+
const content = [
|
|
15098
|
+
{
|
|
15099
|
+
type: "image",
|
|
15100
|
+
data: result.image,
|
|
15101
|
+
mimeType: "image/png"
|
|
15102
|
+
},
|
|
15103
|
+
{
|
|
15104
|
+
type: "text",
|
|
15105
|
+
text: `After ${interactions.length} interaction(s) \u2014 ${result.width}x${result.height}`
|
|
15106
|
+
}
|
|
15107
|
+
];
|
|
15108
|
+
return { content };
|
|
15109
|
+
} catch (error51) {
|
|
15110
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15111
|
+
return {
|
|
15112
|
+
content: [
|
|
15113
|
+
{
|
|
15114
|
+
type: "text",
|
|
15115
|
+
text: `Interaction render failed: ${msg}`
|
|
15116
|
+
}
|
|
15117
|
+
],
|
|
15118
|
+
isError: true
|
|
15119
|
+
};
|
|
15120
|
+
}
|
|
15121
|
+
}
|
|
15122
|
+
);
|
|
15123
|
+
server2.tool(
|
|
15124
|
+
"render_grid",
|
|
15125
|
+
"Render multiple code snippets in a grid layout and return a single combined image. Use this to compare components side-by-side (e.g. design system overview, theme comparison).",
|
|
15126
|
+
{
|
|
15127
|
+
cells: external_exports.array(
|
|
15128
|
+
external_exports.object({
|
|
15129
|
+
label: external_exports.string().describe("Label for this cell"),
|
|
15130
|
+
code: external_exports.string().describe("Component code for this cell")
|
|
15131
|
+
})
|
|
15132
|
+
).describe("Array of cells to render in the grid"),
|
|
15133
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
|
|
15134
|
+
columns: external_exports.number().optional().default(3).describe("Number of columns in the grid"),
|
|
15135
|
+
cellWidth: external_exports.number().optional().default(400).describe("Width of each cell (px)"),
|
|
15136
|
+
cellHeight: external_exports.number().optional().default(300).describe("Height of each cell (px)")
|
|
15137
|
+
},
|
|
15138
|
+
async ({ cells, framework, columns, cellWidth, cellHeight }) => {
|
|
15139
|
+
try {
|
|
15140
|
+
const result = await useCase.renderGrid(cells, framework, {
|
|
15141
|
+
columns,
|
|
15142
|
+
viewport: { width: cellWidth, height: cellHeight }
|
|
15143
|
+
});
|
|
15144
|
+
const content = [
|
|
15145
|
+
{
|
|
15146
|
+
type: "image",
|
|
15147
|
+
data: result.image,
|
|
15148
|
+
mimeType: "image/png"
|
|
15149
|
+
},
|
|
15150
|
+
{
|
|
15151
|
+
type: "text",
|
|
15152
|
+
text: `Grid: ${result.cells} cells, ${columns} columns \u2014 ${result.width}x${result.height}`
|
|
15153
|
+
}
|
|
15154
|
+
];
|
|
15155
|
+
return { content };
|
|
15156
|
+
} catch (error51) {
|
|
15157
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15158
|
+
return {
|
|
15159
|
+
content: [
|
|
15160
|
+
{ type: "text", text: `Grid render failed: ${msg}` }
|
|
15161
|
+
],
|
|
15162
|
+
isError: true
|
|
15163
|
+
};
|
|
15164
|
+
}
|
|
15165
|
+
}
|
|
15166
|
+
);
|
|
15167
|
+
server2.tool(
|
|
15168
|
+
"render_theme",
|
|
15169
|
+
"Render a component in both light and dark mode side-by-side. Returns 2 labeled screenshots for quick theme verification.",
|
|
15170
|
+
{
|
|
15171
|
+
code: external_exports.string().describe("Component code to render"),
|
|
15172
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
|
|
15173
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15174
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15175
|
+
},
|
|
15176
|
+
async ({ code, framework, width, height }) => {
|
|
15177
|
+
try {
|
|
15178
|
+
const start = performance.now();
|
|
15179
|
+
const [lightResults, darkResults] = await Promise.all([
|
|
15180
|
+
useCase.render(code, framework, {
|
|
15181
|
+
viewport: { width, height },
|
|
15182
|
+
fullPage: true,
|
|
15183
|
+
engines: ["chromium"],
|
|
15184
|
+
darkMode: false
|
|
15185
|
+
}),
|
|
15186
|
+
useCase.render(code, framework, {
|
|
15187
|
+
viewport: { width, height },
|
|
15188
|
+
fullPage: true,
|
|
15189
|
+
engines: ["chromium"],
|
|
15190
|
+
darkMode: true
|
|
15191
|
+
})
|
|
15192
|
+
]);
|
|
15193
|
+
const elapsed = Math.round(performance.now() - start);
|
|
15194
|
+
const content = [
|
|
15195
|
+
{ type: "text", text: "\u2600\uFE0F Light mode:" },
|
|
15196
|
+
{
|
|
15197
|
+
type: "image",
|
|
15198
|
+
data: lightResults[0].image,
|
|
15199
|
+
mimeType: "image/png"
|
|
15200
|
+
},
|
|
15201
|
+
{ type: "text", text: "\u{1F319} Dark mode:" },
|
|
15202
|
+
{
|
|
15203
|
+
type: "image",
|
|
15204
|
+
data: darkResults[0].image,
|
|
15205
|
+
mimeType: "image/png"
|
|
15206
|
+
},
|
|
15207
|
+
{
|
|
15208
|
+
type: "text",
|
|
15209
|
+
text: `${lightResults[0].width}x${lightResults[0].height} (${elapsed}ms)`
|
|
15210
|
+
}
|
|
15211
|
+
];
|
|
15212
|
+
return { content };
|
|
15213
|
+
} catch (error51) {
|
|
15214
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15215
|
+
return {
|
|
15216
|
+
content: [
|
|
15217
|
+
{ type: "text", text: `Theme render failed: ${msg}` }
|
|
15218
|
+
],
|
|
15219
|
+
isError: true
|
|
15220
|
+
};
|
|
15221
|
+
}
|
|
15222
|
+
}
|
|
15223
|
+
);
|
|
15224
|
+
server2.tool(
|
|
15225
|
+
"render_matrix",
|
|
15226
|
+
"Render a component across multiple viewports AND themes in one call. Returns a grid of screenshots (viewports x themes) for comprehensive responsive + theme verification.",
|
|
15227
|
+
{
|
|
15228
|
+
code: external_exports.string().describe("Component code to render"),
|
|
15229
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
15230
|
+
viewports: external_exports.array(
|
|
15231
|
+
external_exports.union([
|
|
15232
|
+
external_exports.enum(["mobile", "tablet", "desktop"]),
|
|
15233
|
+
external_exports.object({
|
|
15234
|
+
width: external_exports.number().describe("Viewport width (px)"),
|
|
15235
|
+
height: external_exports.number().describe("Viewport height (px)")
|
|
15236
|
+
})
|
|
15237
|
+
])
|
|
15238
|
+
).default(["mobile", "tablet", "desktop"]).describe(
|
|
15239
|
+
"Viewports to render: preset names ('mobile','tablet','desktop') or custom {width, height}"
|
|
15240
|
+
),
|
|
15241
|
+
themes: external_exports.array(external_exports.enum(["light", "dark"])).default(["light", "dark"]).describe("Themes to render for each viewport"),
|
|
15242
|
+
css: external_exports.string().optional().describe("Custom CSS to inject (design tokens, variables, etc)"),
|
|
15243
|
+
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
|
|
15244
|
+
},
|
|
15245
|
+
async ({ code, framework, viewports, themes, css, tailwindVersion }) => {
|
|
15246
|
+
try {
|
|
15247
|
+
const start = performance.now();
|
|
15248
|
+
const resolvedViewports = viewports.map((v) => {
|
|
15249
|
+
if (typeof v === "string") {
|
|
15250
|
+
const preset = DEVICE_PRESETS[v];
|
|
15251
|
+
return { label: v, width: preset.width, height: preset.height };
|
|
15252
|
+
}
|
|
15253
|
+
return {
|
|
15254
|
+
label: `${v.width}x${v.height}`,
|
|
15255
|
+
width: v.width,
|
|
15256
|
+
height: v.height
|
|
15257
|
+
};
|
|
15258
|
+
});
|
|
15259
|
+
const results = await useCase.renderMatrix(
|
|
15260
|
+
code,
|
|
15261
|
+
framework,
|
|
15262
|
+
resolvedViewports,
|
|
15263
|
+
themes,
|
|
15264
|
+
{
|
|
15265
|
+
css,
|
|
15266
|
+
tailwindVersion
|
|
15267
|
+
}
|
|
15268
|
+
);
|
|
15269
|
+
const elapsed = Math.round(performance.now() - start);
|
|
15270
|
+
const content = results.flatMap((r) => [
|
|
15271
|
+
{
|
|
15272
|
+
type: "image",
|
|
15273
|
+
data: r.image,
|
|
15274
|
+
mimeType: "image/png"
|
|
15275
|
+
},
|
|
15276
|
+
{
|
|
15277
|
+
type: "text",
|
|
15278
|
+
text: `[${r.viewport}/${r.theme}] ${r.width}x${r.height}${r.consoleErrors.length ? `
|
|
15279
|
+
\u26A0\uFE0F Console errors:
|
|
15280
|
+
${r.consoleErrors.join("\n")}` : ""}`
|
|
15281
|
+
}
|
|
15282
|
+
]);
|
|
15283
|
+
content.push({
|
|
14696
15284
|
type: "text",
|
|
14697
|
-
text: `
|
|
14698
|
-
}
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
15285
|
+
text: `Matrix: ${resolvedViewports.length} viewports x ${themes.length} themes = ${results.length} renders (${elapsed}ms)`
|
|
15286
|
+
});
|
|
15287
|
+
return { content };
|
|
15288
|
+
} catch (error51) {
|
|
15289
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15290
|
+
return {
|
|
15291
|
+
content: [
|
|
15292
|
+
{ type: "text", text: `Matrix render failed: ${msg}` }
|
|
15293
|
+
],
|
|
15294
|
+
isError: true
|
|
15295
|
+
};
|
|
15296
|
+
}
|
|
14704
15297
|
}
|
|
14705
|
-
|
|
15298
|
+
);
|
|
15299
|
+
server2.tool(
|
|
15300
|
+
"capture_animation",
|
|
15301
|
+
"Capture multiple frames of a CSS animation or transition over time. Returns sequential screenshots to verify animation behavior, timing, and smoothness.",
|
|
15302
|
+
{
|
|
15303
|
+
code: external_exports.string().describe("Component code with CSS animations/transitions"),
|
|
15304
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
15305
|
+
frames: external_exports.number().optional().default(5).describe("Number of frames to capture"),
|
|
15306
|
+
duration: external_exports.number().optional().default(1e3).describe("Total capture duration in ms"),
|
|
15307
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15308
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15309
|
+
},
|
|
15310
|
+
async ({ code, framework, frames, duration: duration3, width, height }) => {
|
|
15311
|
+
try {
|
|
15312
|
+
const results = await useCase.captureAnimation(code, framework, {
|
|
15313
|
+
frames,
|
|
15314
|
+
duration: duration3,
|
|
15315
|
+
viewport: { width, height }
|
|
15316
|
+
});
|
|
15317
|
+
const content = results.flatMap((r) => [
|
|
15318
|
+
{
|
|
15319
|
+
type: "image",
|
|
15320
|
+
data: r.image,
|
|
15321
|
+
mimeType: "image/png"
|
|
15322
|
+
},
|
|
15323
|
+
{
|
|
15324
|
+
type: "text",
|
|
15325
|
+
text: `[${r.timestamp}ms]`
|
|
15326
|
+
}
|
|
15327
|
+
]);
|
|
15328
|
+
return { content };
|
|
15329
|
+
} catch (error51) {
|
|
15330
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15331
|
+
return {
|
|
15332
|
+
content: [
|
|
15333
|
+
{
|
|
15334
|
+
type: "text",
|
|
15335
|
+
text: `Animation capture failed: ${msg}`
|
|
15336
|
+
}
|
|
15337
|
+
],
|
|
15338
|
+
isError: true
|
|
15339
|
+
};
|
|
15340
|
+
}
|
|
15341
|
+
}
|
|
15342
|
+
);
|
|
15343
|
+
}
|
|
15344
|
+
|
|
15345
|
+
// src/tools/screenshot-tools.ts
|
|
15346
|
+
function registerScreenshotTools(server2, useCase) {
|
|
15347
|
+
server2.tool(
|
|
15348
|
+
"screenshot_url",
|
|
15349
|
+
"Take a screenshot of a URL (e.g. localhost:3000) across multiple browser engines in parallel. Supports waiting for specific elements and retry with exponential backoff for dev servers that may be slow to start.",
|
|
15350
|
+
{
|
|
15351
|
+
url: external_exports.string().url().describe("URL to screenshot"),
|
|
15352
|
+
engines: external_exports.array(external_exports.enum(["chromium", "firefox", "webkit"])).optional().default(["chromium"]).describe("Browser engines"),
|
|
15353
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15354
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
15355
|
+
fullPage: external_exports.boolean().optional().default(true).describe("Capture full scroll height"),
|
|
15356
|
+
waitForSelector: external_exports.string().optional().describe(
|
|
15357
|
+
"Wait for a specific CSS selector to appear before capturing (e.g. '#app', '.loaded')"
|
|
15358
|
+
),
|
|
15359
|
+
waitForNetworkIdle: external_exports.boolean().optional().default(true).describe("Wait for network to settle before capturing"),
|
|
15360
|
+
retryOnError: external_exports.number().optional().default(0).describe(
|
|
15361
|
+
"Number of retries with exponential backoff (300ms, 600ms, 1200ms...) if the page fails to load"
|
|
15362
|
+
)
|
|
15363
|
+
},
|
|
15364
|
+
async ({
|
|
15365
|
+
url: url2,
|
|
15366
|
+
engines,
|
|
15367
|
+
width,
|
|
15368
|
+
height,
|
|
15369
|
+
fullPage,
|
|
15370
|
+
waitForSelector,
|
|
15371
|
+
waitForNetworkIdle,
|
|
15372
|
+
retryOnError
|
|
15373
|
+
}) => {
|
|
15374
|
+
try {
|
|
15375
|
+
const results = await useCase.screenshotUrlWithRetry(url2, {
|
|
15376
|
+
viewport: { width, height },
|
|
15377
|
+
engines,
|
|
15378
|
+
fullPage,
|
|
15379
|
+
waitForSelector,
|
|
15380
|
+
waitForNetworkIdle,
|
|
15381
|
+
retryCount: retryOnError
|
|
15382
|
+
});
|
|
15383
|
+
const content = results.flatMap((r) => [
|
|
15384
|
+
{
|
|
15385
|
+
type: "image",
|
|
15386
|
+
data: r.image,
|
|
15387
|
+
mimeType: "image/png"
|
|
15388
|
+
},
|
|
15389
|
+
{
|
|
15390
|
+
type: "text",
|
|
15391
|
+
text: `[${r.engine}] ${r.width}x${r.height}${r.consoleErrors.length ? `
|
|
15392
|
+
\u26A0\uFE0F Console errors:
|
|
15393
|
+
${r.consoleErrors.join("\n")}` : ""}`
|
|
15394
|
+
}
|
|
15395
|
+
]);
|
|
15396
|
+
return { content };
|
|
15397
|
+
} catch (error51) {
|
|
15398
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15399
|
+
const retryInfo = retryOnError > 0 ? ` (after ${retryOnError + 1} attempts)` : "";
|
|
15400
|
+
return {
|
|
15401
|
+
content: [
|
|
15402
|
+
{
|
|
15403
|
+
type: "text",
|
|
15404
|
+
text: `Screenshot failed${retryInfo}: ${msg}`
|
|
15405
|
+
}
|
|
15406
|
+
],
|
|
15407
|
+
isError: true
|
|
15408
|
+
};
|
|
15409
|
+
}
|
|
15410
|
+
}
|
|
15411
|
+
);
|
|
15412
|
+
}
|
|
15413
|
+
|
|
15414
|
+
// src/tools/snapshot-tools.ts
|
|
15415
|
+
function registerSnapshotTools(server2, useCase) {
|
|
15416
|
+
server2.tool(
|
|
15417
|
+
"snapshot_save",
|
|
15418
|
+
"Render a component and save the screenshot as a named snapshot. Use this to establish a baseline before making changes. Later use snapshot_check to detect visual regressions.",
|
|
15419
|
+
{
|
|
15420
|
+
name: external_exports.string().describe(
|
|
15421
|
+
"Unique name for this snapshot (e.g. 'header-desktop', 'button-hover')"
|
|
15422
|
+
),
|
|
15423
|
+
code: external_exports.string().describe("Component code to render and snapshot"),
|
|
15424
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
|
|
15425
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15426
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
15427
|
+
darkMode: external_exports.boolean().optional().default(false).describe("Render with dark mode"),
|
|
15428
|
+
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
|
|
15429
|
+
},
|
|
15430
|
+
async ({
|
|
15431
|
+
name,
|
|
15432
|
+
code,
|
|
15433
|
+
framework,
|
|
15434
|
+
width,
|
|
15435
|
+
height,
|
|
15436
|
+
darkMode,
|
|
15437
|
+
tailwindVersion
|
|
15438
|
+
}) => {
|
|
15439
|
+
try {
|
|
15440
|
+
const result = await useCase.save(name, code, framework, {
|
|
15441
|
+
viewport: { width, height },
|
|
15442
|
+
darkMode,
|
|
15443
|
+
tailwindVersion
|
|
15444
|
+
});
|
|
15445
|
+
return {
|
|
15446
|
+
content: [
|
|
15447
|
+
{
|
|
15448
|
+
type: "image",
|
|
15449
|
+
data: result.image,
|
|
15450
|
+
mimeType: "image/png"
|
|
15451
|
+
},
|
|
15452
|
+
{
|
|
15453
|
+
type: "text",
|
|
15454
|
+
text: `Snapshot "${name}" saved (${result.width}x${result.height})`
|
|
15455
|
+
}
|
|
15456
|
+
]
|
|
15457
|
+
};
|
|
15458
|
+
} catch (error51) {
|
|
15459
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15460
|
+
return {
|
|
15461
|
+
content: [
|
|
15462
|
+
{ type: "text", text: `Snapshot save failed: ${msg}` }
|
|
15463
|
+
],
|
|
15464
|
+
isError: true
|
|
15465
|
+
};
|
|
15466
|
+
}
|
|
15467
|
+
}
|
|
15468
|
+
);
|
|
15469
|
+
server2.tool(
|
|
15470
|
+
"snapshot_check",
|
|
15471
|
+
"Render a component and compare against a previously saved snapshot. Returns pixel diff and pass/fail. Use this after making changes to verify no visual regression.",
|
|
15472
|
+
{
|
|
15473
|
+
name: external_exports.string().describe("Name of the snapshot to compare against"),
|
|
15474
|
+
code: external_exports.string().describe("Current component code to render and compare"),
|
|
15475
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
|
|
15476
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15477
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
15478
|
+
darkMode: external_exports.boolean().optional().default(false).describe("Render with dark mode"),
|
|
15479
|
+
tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
|
|
15480
|
+
},
|
|
15481
|
+
async ({
|
|
15482
|
+
name,
|
|
15483
|
+
code,
|
|
15484
|
+
framework,
|
|
15485
|
+
width,
|
|
15486
|
+
height,
|
|
15487
|
+
darkMode,
|
|
15488
|
+
tailwindVersion
|
|
15489
|
+
}) => {
|
|
15490
|
+
try {
|
|
15491
|
+
const result = await useCase.check(name, code, framework, {
|
|
15492
|
+
viewport: { width, height },
|
|
15493
|
+
darkMode,
|
|
15494
|
+
tailwindVersion
|
|
15495
|
+
});
|
|
15496
|
+
if (!result) {
|
|
15497
|
+
return {
|
|
15498
|
+
content: [
|
|
15499
|
+
{
|
|
15500
|
+
type: "text",
|
|
15501
|
+
text: `No snapshot found with name "${name}". Use snapshot_save first.`
|
|
15502
|
+
}
|
|
15503
|
+
],
|
|
15504
|
+
isError: true
|
|
15505
|
+
};
|
|
15506
|
+
}
|
|
15507
|
+
const status = result.passed ? "PASS" : "FAIL";
|
|
15508
|
+
const content = [
|
|
15509
|
+
{
|
|
15510
|
+
type: "text",
|
|
15511
|
+
text: `${status}: ${result.diffPercentage}% pixels differ vs snapshot "${name}"`
|
|
15512
|
+
},
|
|
15513
|
+
{
|
|
15514
|
+
type: "image",
|
|
15515
|
+
data: result.rendered,
|
|
15516
|
+
mimeType: "image/png"
|
|
15517
|
+
},
|
|
15518
|
+
{ type: "text", text: "Diff:" },
|
|
15519
|
+
{
|
|
15520
|
+
type: "image",
|
|
15521
|
+
data: result.diff,
|
|
15522
|
+
mimeType: "image/png"
|
|
15523
|
+
}
|
|
15524
|
+
];
|
|
15525
|
+
return { content };
|
|
15526
|
+
} catch (error51) {
|
|
15527
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15528
|
+
return {
|
|
15529
|
+
content: [
|
|
15530
|
+
{ type: "text", text: `Snapshot check failed: ${msg}` }
|
|
15531
|
+
],
|
|
15532
|
+
isError: true
|
|
15533
|
+
};
|
|
15534
|
+
}
|
|
15535
|
+
}
|
|
15536
|
+
);
|
|
15537
|
+
server2.tool(
|
|
15538
|
+
"snapshot_list",
|
|
15539
|
+
"List all saved snapshots with their names and timestamps.",
|
|
15540
|
+
{},
|
|
15541
|
+
async () => {
|
|
15542
|
+
const snapshots = useCase.list();
|
|
15543
|
+
if (snapshots.length === 0) {
|
|
15544
|
+
return {
|
|
15545
|
+
content: [
|
|
15546
|
+
{
|
|
15547
|
+
type: "text",
|
|
15548
|
+
text: "No snapshots saved. Use snapshot_save to create one."
|
|
15549
|
+
}
|
|
15550
|
+
]
|
|
15551
|
+
};
|
|
15552
|
+
}
|
|
15553
|
+
const lines = snapshots.map(
|
|
15554
|
+
(s) => `\u2022 ${s.key} (saved ${new Date(s.timestamp).toLocaleTimeString()})`
|
|
15555
|
+
);
|
|
15556
|
+
return {
|
|
15557
|
+
content: [
|
|
15558
|
+
{
|
|
15559
|
+
type: "text",
|
|
15560
|
+
text: `Saved snapshots (${snapshots.length}):
|
|
15561
|
+
${lines.join("\n")}`
|
|
15562
|
+
}
|
|
15563
|
+
]
|
|
15564
|
+
};
|
|
15565
|
+
}
|
|
15566
|
+
);
|
|
15567
|
+
}
|
|
15568
|
+
|
|
15569
|
+
// src/tools/register.ts
|
|
15570
|
+
function registerAllTools(server2, useCases) {
|
|
15571
|
+
registerRenderTools(server2, useCases.render);
|
|
15572
|
+
registerScreenshotTools(server2, useCases.screenshot);
|
|
15573
|
+
registerDiffTools(server2, useCases.diff);
|
|
15574
|
+
registerAuditTools(server2, useCases.audit);
|
|
15575
|
+
registerSnapshotTools(server2, useCases.snapshot);
|
|
15576
|
+
registerCatalogTools(server2, useCases.catalog);
|
|
15577
|
+
}
|
|
15578
|
+
|
|
15579
|
+
// src/index.ts
|
|
15580
|
+
var browserPool = new BrowserPool();
|
|
15581
|
+
var htmlBuilder = new HtmlBuilder();
|
|
15582
|
+
var imageComparator = new ImageComparator();
|
|
15583
|
+
var snapshotStore = new SnapshotStore();
|
|
15584
|
+
var renderUseCase = new RenderUseCase(
|
|
15585
|
+
browserPool,
|
|
15586
|
+
htmlBuilder,
|
|
15587
|
+
imageComparator
|
|
15588
|
+
);
|
|
15589
|
+
var screenshotUseCase = new ScreenshotUseCase(browserPool);
|
|
15590
|
+
var diffUseCase = new DiffUseCase(renderUseCase, imageComparator);
|
|
15591
|
+
var auditUseCase = new AuditUseCase(browserPool, htmlBuilder);
|
|
15592
|
+
var snapshotUseCase = new SnapshotUseCase(
|
|
15593
|
+
snapshotStore,
|
|
15594
|
+
renderUseCase,
|
|
15595
|
+
diffUseCase
|
|
14706
15596
|
);
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
engines: external_exports.array(external_exports.enum(["chromium", "firefox", "webkit"])).optional().default(["chromium"]).describe("Browser engines"),
|
|
14713
|
-
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14714
|
-
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
14715
|
-
fullPage: external_exports.boolean().optional().default(true).describe("Capture full scroll height")
|
|
14716
|
-
},
|
|
14717
|
-
async ({ url: url2, engines, width, height, fullPage }) => {
|
|
15597
|
+
var catalogUseCase = new CatalogUseCase(renderUseCase);
|
|
15598
|
+
async function ensureBrowser() {
|
|
15599
|
+
try {
|
|
15600
|
+
await browserPool.warmup(["chromium"]);
|
|
15601
|
+
} catch {
|
|
14718
15602
|
try {
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
fullPage,
|
|
14723
|
-
engines
|
|
14724
|
-
});
|
|
14725
|
-
const content = results.flatMap((r) => [
|
|
14726
|
-
{
|
|
14727
|
-
type: "image",
|
|
14728
|
-
data: r.image,
|
|
14729
|
-
mimeType: "image/png"
|
|
14730
|
-
},
|
|
14731
|
-
{
|
|
14732
|
-
type: "text",
|
|
14733
|
-
text: `[${r.engine}] ${r.width}x${r.height}`
|
|
14734
|
-
}
|
|
14735
|
-
]);
|
|
14736
|
-
return { content };
|
|
14737
|
-
} catch (error51) {
|
|
14738
|
-
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14739
|
-
return { content: [{ type: "text", text: `Screenshot failed: ${msg}` }], isError: true };
|
|
15603
|
+
execSync("npx playwright install chromium", { stdio: "pipe" });
|
|
15604
|
+
await browserPool.warmup(["chromium"]);
|
|
15605
|
+
} catch {
|
|
14740
15606
|
}
|
|
14741
15607
|
}
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
15608
|
+
}
|
|
15609
|
+
await ensureBrowser();
|
|
15610
|
+
var server = new McpServer({
|
|
15611
|
+
name: "frameshot",
|
|
15612
|
+
version: "0.5.0"
|
|
15613
|
+
});
|
|
15614
|
+
registerAllTools(server, {
|
|
15615
|
+
render: renderUseCase,
|
|
15616
|
+
screenshot: screenshotUseCase,
|
|
15617
|
+
diff: diffUseCase,
|
|
15618
|
+
audit: auditUseCase,
|
|
15619
|
+
snapshot: snapshotUseCase,
|
|
15620
|
+
catalog: catalogUseCase
|
|
14745
15621
|
});
|
|
15622
|
+
var transport = new StdioServerTransport();
|
|
14746
15623
|
await server.connect(transport);
|
|
14747
15624
|
process.on("SIGINT", async () => {
|
|
14748
|
-
await shutdown();
|
|
15625
|
+
await browserPool.shutdown();
|
|
14749
15626
|
process.exit(0);
|
|
14750
15627
|
});
|
|
14751
15628
|
process.on("SIGTERM", async () => {
|
|
14752
|
-
await shutdown();
|
|
15629
|
+
await browserPool.shutdown();
|
|
14753
15630
|
process.exit(0);
|
|
14754
15631
|
});
|