frameshot-mcp 0.3.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 +85 -32
- package/action.yml +130 -0
- package/dist/chunk-47YJG5HR.js +690 -0
- package/dist/index.js +1058 -653
- package/dist/renderer.d.ts +241 -0
- package/dist/renderer.js +28 -0
- package/package.json +19 -4
- package/scripts/render-changed.mjs +90 -0
- package/scripts/setup-labels.sh +48 -0
package/dist/index.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
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
|
|
9
19
|
import { execSync } from "child_process";
|
|
@@ -14524,703 +14534,1098 @@ function date4(params) {
|
|
|
14524
14534
|
// node_modules/zod/v4/classic/external.js
|
|
14525
14535
|
config(en_default());
|
|
14526
14536
|
|
|
14527
|
-
// src/
|
|
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
|
-
const context = await browser.newContext({
|
|
14554
|
-
viewport: { width: 1280, height: 800 },
|
|
14555
|
-
deviceScaleFactor: 2
|
|
14556
|
-
});
|
|
14557
|
-
const page = await context.newPage();
|
|
14558
|
-
await page.setContent(
|
|
14559
|
-
'<html><head><script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script></head><body></body></html>',
|
|
14560
|
-
{ waitUntil: "networkidle" }
|
|
14561
|
-
);
|
|
14562
|
-
const slot = { browser, page, ready: true };
|
|
14563
|
-
pool.set(engine, slot);
|
|
14564
|
-
return slot;
|
|
14565
|
-
}
|
|
14566
|
-
function wrapComponent(code, framework, darkMode = false, css = "") {
|
|
14567
|
-
const tailwind = '<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>';
|
|
14568
|
-
const tailwindConfig = `<script>tailwind.config={darkMode:'class'}</script>`;
|
|
14569
|
-
const customCss = css ? `<style>${css}</style>` : "";
|
|
14570
|
-
const baseStyle = `<style>*{margin:0;box-sizing:border-box}body{padding:16px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}</style>`;
|
|
14571
|
-
const htmlClass = darkMode ? ' class="dark"' : "";
|
|
14572
|
-
if (framework === "html") {
|
|
14573
|
-
if (code.includes("<html")) return code;
|
|
14574
|
-
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}${baseStyle}${customCss}</head><body>${code}</body></html>`;
|
|
14575
|
-
}
|
|
14576
|
-
if (framework === "react") {
|
|
14577
|
-
const cleanedCode = code.replace(/['"]use client['"];?\n?/g, "").replace(/['"]use server['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/image['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/link['"];?\n?/g, "");
|
|
14578
|
-
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
14579
|
-
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
14580
|
-
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
14581
|
-
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
14582
|
-
${baseStyle}${customCss}</head><body><div id="root"></div>
|
|
14583
|
-
<script type="text/babel">
|
|
14584
|
-
const Image = (props) => React.createElement('img', {...props, src: props.src?.src || props.src});
|
|
14585
|
-
const Link = ({href, children, ...props}) => React.createElement('a', {href, ...props}, children);
|
|
14586
|
-
${cleanedCode}
|
|
14587
|
-
const _C = typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:typeof Default!=='undefined'?Default:null;
|
|
14588
|
-
if(_C)ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(_C));
|
|
14589
|
-
</script></body></html>`;
|
|
14590
|
-
}
|
|
14591
|
-
if (framework === "vue") {
|
|
14592
|
-
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
14593
|
-
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
14594
|
-
${baseStyle}${customCss}</head><body><div id="app"></div>
|
|
14595
|
-
<script>
|
|
14596
|
-
const{createApp,ref,reactive,computed,onMounted,watch,watchEffect}=Vue;
|
|
14597
|
-
${code}
|
|
14598
|
-
const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
|
|
14599
|
-
if(_C)createApp(_C).mount('#app');
|
|
14600
|
-
</script></body></html>`;
|
|
14601
|
-
}
|
|
14602
|
-
if (framework === "svelte") {
|
|
14603
|
-
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
14604
|
-
<script src="https://unpkg.com/svelte@4/compiler.cjs"></script>
|
|
14605
|
-
${baseStyle}${customCss}</head><body><div id="app"></div>
|
|
14606
|
-
<script type="module">
|
|
14607
|
-
import "https://unpkg.com/svelte@4/internal/index.mjs";
|
|
14608
|
-
${code}
|
|
14609
|
-
const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
|
|
14610
|
-
if(_C)new _C({target:document.getElementById('app')});
|
|
14611
|
-
</script></body></html>`;
|
|
14612
|
-
}
|
|
14613
|
-
return code;
|
|
14614
|
-
}
|
|
14615
|
-
async function renderSingle(engine, html, options) {
|
|
14616
|
-
const { width = 1280, height = 800, fullPage = true, waitFor = 0 } = options;
|
|
14617
|
-
const slot = await getSlot(engine);
|
|
14618
|
-
const { page } = slot;
|
|
14619
|
-
const consoleErrors = [];
|
|
14620
|
-
const onError = (msg) => {
|
|
14621
|
-
if (msg.type() === "error") {
|
|
14622
|
-
consoleErrors.push(msg.text());
|
|
14623
|
-
}
|
|
14624
|
-
};
|
|
14625
|
-
page.on("console", onError);
|
|
14626
|
-
page.on("pageerror", (err) => consoleErrors.push(err.message));
|
|
14627
|
-
const currentViewport = page.viewportSize();
|
|
14628
|
-
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14629
|
-
await page.setViewportSize({ width, height });
|
|
14630
|
-
}
|
|
14631
|
-
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
14632
|
-
if (waitFor > 0) {
|
|
14633
|
-
await page.waitForTimeout(waitFor);
|
|
14634
|
-
}
|
|
14635
|
-
const screenshot = await page.screenshot({ type: "png", fullPage });
|
|
14636
|
-
const metrics = await page.evaluate(() => ({
|
|
14637
|
-
w: document.documentElement.scrollWidth,
|
|
14638
|
-
h: document.documentElement.scrollHeight
|
|
14639
|
-
}));
|
|
14640
|
-
page.removeListener("console", onError);
|
|
14641
|
-
return {
|
|
14642
|
-
engine,
|
|
14643
|
-
image: screenshot.toString("base64"),
|
|
14644
|
-
width: metrics.w,
|
|
14645
|
-
height: metrics.h,
|
|
14646
|
-
consoleErrors
|
|
14647
|
-
};
|
|
14648
|
-
}
|
|
14649
|
-
async function render(code, framework, options = {}) {
|
|
14650
|
-
const engines = options.engines ?? ["chromium"];
|
|
14651
|
-
const html = wrapComponent(
|
|
14652
|
-
code,
|
|
14653
|
-
framework,
|
|
14654
|
-
options.darkMode ?? false,
|
|
14655
|
-
options.css ?? ""
|
|
14656
|
-
);
|
|
14657
|
-
const results = await Promise.all(
|
|
14658
|
-
engines.map((e) => renderSingle(e, html, options))
|
|
14659
|
-
);
|
|
14660
|
-
return results;
|
|
14661
|
-
}
|
|
14662
|
-
async function screenshotUrl(url2, options = {}) {
|
|
14663
|
-
const engines = options.engines ?? ["chromium"];
|
|
14664
|
-
const results = await Promise.all(
|
|
14665
|
-
engines.map(async (engine) => {
|
|
14666
|
-
const {
|
|
14667
|
-
width = 1280,
|
|
14668
|
-
height = 800,
|
|
14669
|
-
fullPage = true,
|
|
14670
|
-
waitFor = 0
|
|
14671
|
-
} = options;
|
|
14672
|
-
const slot = await getSlot(engine);
|
|
14673
|
-
const { page } = slot;
|
|
14674
|
-
const consoleErrors = [];
|
|
14675
|
-
const onError = (msg) => {
|
|
14676
|
-
if (msg.type() === "error") {
|
|
14677
|
-
consoleErrors.push(msg.text());
|
|
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
|
+
};
|
|
14678
14562
|
}
|
|
14679
|
-
|
|
14680
|
-
|
|
14681
|
-
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
}
|
|
14709
|
-
async function auditA11y(code, framework, options = {}) {
|
|
14710
|
-
const html = wrapComponent(
|
|
14711
|
-
code,
|
|
14712
|
-
framework,
|
|
14713
|
-
options.darkMode ?? false,
|
|
14714
|
-
options.css ?? ""
|
|
14715
|
-
);
|
|
14716
|
-
const slot = await getSlot("chromium");
|
|
14717
|
-
const { page } = slot;
|
|
14718
|
-
const { width = 1280, height = 800 } = options;
|
|
14719
|
-
const currentViewport = page.viewportSize();
|
|
14720
|
-
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14721
|
-
await page.setViewportSize({ width, height });
|
|
14722
|
-
}
|
|
14723
|
-
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
14724
|
-
const axeSource = await import("axe-core").then((m) => m.source);
|
|
14725
|
-
await page.addScriptTag({ content: axeSource });
|
|
14726
|
-
const results = await page.evaluate(() => {
|
|
14727
|
-
return window.axe.run();
|
|
14728
|
-
});
|
|
14729
|
-
return {
|
|
14730
|
-
violations: results.violations.map(
|
|
14731
|
-
(v) => ({
|
|
14732
|
-
id: v.id,
|
|
14733
|
-
impact: v.impact,
|
|
14734
|
-
description: v.description,
|
|
14735
|
-
helpUrl: v.helpUrl,
|
|
14736
|
-
nodes: v.nodes.map((n) => ({
|
|
14737
|
-
html: n.html,
|
|
14738
|
-
target: n.target
|
|
14739
|
-
}))
|
|
14740
|
-
})
|
|
14741
|
-
),
|
|
14742
|
-
passes: results.passes.length,
|
|
14743
|
-
incomplete: results.incomplete.length
|
|
14744
|
-
};
|
|
14745
|
-
}
|
|
14746
|
-
async function diffComponent(before, after, framework, options = {}) {
|
|
14747
|
-
const [beforeResults, afterResults] = await Promise.all([
|
|
14748
|
-
render(before, framework, { ...options, engines: ["chromium"] }),
|
|
14749
|
-
render(after, framework, { ...options, engines: ["chromium"] })
|
|
14750
|
-
]);
|
|
14751
|
-
const beforeBuf = Buffer.from(beforeResults[0].image, "base64");
|
|
14752
|
-
const afterBuf = Buffer.from(afterResults[0].image, "base64");
|
|
14753
|
-
const beforePng = PNG.sync.read(beforeBuf);
|
|
14754
|
-
const afterPng = PNG.sync.read(afterBuf);
|
|
14755
|
-
const width = Math.max(beforePng.width, afterPng.width);
|
|
14756
|
-
const height = Math.max(beforePng.height, afterPng.height);
|
|
14757
|
-
const normalizedBefore = new PNG({ width, height });
|
|
14758
|
-
const normalizedAfter = new PNG({ width, height });
|
|
14759
|
-
PNG.bitblt(
|
|
14760
|
-
beforePng,
|
|
14761
|
-
normalizedBefore,
|
|
14762
|
-
0,
|
|
14763
|
-
0,
|
|
14764
|
-
beforePng.width,
|
|
14765
|
-
beforePng.height,
|
|
14766
|
-
0,
|
|
14767
|
-
0
|
|
14768
|
-
);
|
|
14769
|
-
PNG.bitblt(
|
|
14770
|
-
afterPng,
|
|
14771
|
-
normalizedAfter,
|
|
14772
|
-
0,
|
|
14773
|
-
0,
|
|
14774
|
-
afterPng.width,
|
|
14775
|
-
afterPng.height,
|
|
14776
|
-
0,
|
|
14777
|
-
0
|
|
14778
|
-
);
|
|
14779
|
-
const diffPng = new PNG({ width, height });
|
|
14780
|
-
const diffPixels = pixelmatch(
|
|
14781
|
-
normalizedBefore.data,
|
|
14782
|
-
normalizedAfter.data,
|
|
14783
|
-
diffPng.data,
|
|
14784
|
-
width,
|
|
14785
|
-
height,
|
|
14786
|
-
{ threshold: 0.1 }
|
|
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
|
+
}
|
|
14787
14593
|
);
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
}
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
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
|
+
}
|
|
14805
14630
|
);
|
|
14806
|
-
const slot = await getSlot("chromium");
|
|
14807
|
-
const { page } = slot;
|
|
14808
|
-
const { width = 1280, height = 800 } = options;
|
|
14809
|
-
const currentViewport = page.viewportSize();
|
|
14810
|
-
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14811
|
-
await page.setViewportSize({ width, height });
|
|
14812
|
-
}
|
|
14813
|
-
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
14814
|
-
const interval = duration3 / (frames - 1);
|
|
14815
|
-
const results = [];
|
|
14816
|
-
for (let i = 0; i < frames; i++) {
|
|
14817
|
-
if (i > 0) {
|
|
14818
|
-
await page.waitForTimeout(interval);
|
|
14819
|
-
}
|
|
14820
|
-
const screenshot = await page.screenshot({ type: "png", fullPage: false });
|
|
14821
|
-
results.push({
|
|
14822
|
-
timestamp: Math.round(i * interval),
|
|
14823
|
-
image: screenshot.toString("base64")
|
|
14824
|
-
});
|
|
14825
|
-
}
|
|
14826
|
-
return results;
|
|
14827
14631
|
}
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
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
|
+
}
|
|
14708
|
+
);
|
|
14834
14709
|
}
|
|
14835
14710
|
|
|
14836
|
-
// src/
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
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
|
+
}
|
|
14845
14760
|
}
|
|
14846
|
-
|
|
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
|
+
);
|
|
14847
14830
|
}
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
schemes.map(
|
|
14887
|
-
(scheme) => render(code, framework, {
|
|
14888
|
-
width,
|
|
14889
|
-
height,
|
|
14890
|
-
fullPage,
|
|
14891
|
-
engines,
|
|
14892
|
-
darkMode: scheme === "dark",
|
|
14893
|
-
css
|
|
14894
|
-
})
|
|
14895
|
-
)
|
|
14896
|
-
);
|
|
14897
|
-
const elapsed = Math.round(performance.now() - start);
|
|
14898
|
-
const results = allResults.flat();
|
|
14899
|
-
const content = results.flatMap((r) => [
|
|
14900
|
-
{
|
|
14901
|
-
type: "image",
|
|
14902
|
-
data: r.image,
|
|
14903
|
-
mimeType: "image/png"
|
|
14904
|
-
},
|
|
14905
|
-
{
|
|
14906
|
-
type: "text",
|
|
14907
|
-
text: `[${r.engine}] ${r.width}x${r.height} (${elapsed}ms)${r.consoleErrors.length ? `
|
|
14831
|
+
|
|
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 ? `
|
|
14908
14869
|
\u26A0\uFE0F Console errors:
|
|
14909
14870
|
${r.consoleErrors.join("\n")}` : ""}`
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
|
|
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
|
+
}
|
|
14919
14883
|
}
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
14923
|
-
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
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 ? `
|
|
14949
14942
|
\u26A0\uFE0F Console errors:
|
|
14950
14943
|
${r.consoleErrors.join("\n")}` : ""}`
|
|
14951
|
-
|
|
14952
|
-
|
|
14953
|
-
|
|
14954
|
-
|
|
14955
|
-
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
|
|
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
|
+
}
|
|
14960
14954
|
}
|
|
14961
|
-
|
|
14962
|
-
|
|
14963
|
-
|
|
14964
|
-
|
|
14965
|
-
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
],
|
|
15008
|
-
isError: true
|
|
15009
|
-
};
|
|
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
|
+
}
|
|
15010
15001
|
}
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
|
|
15017
|
-
|
|
15018
|
-
|
|
15019
|
-
|
|
15020
|
-
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
const wrappedCode = framework === "react" ? `${code}
|
|
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}
|
|
15033
15023
|
const _VARIANT_PROPS = ${variant.props};
|
|
15034
15024
|
function _VariantWrapper() { return <App {..._VARIANT_PROPS} />; }` : code;
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
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)")
|
|
15047
15082
|
})
|
|
15048
|
-
)
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
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({
|
|
15056
15284
|
type: "text",
|
|
15057
|
-
text: `
|
|
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
|
+
}
|
|
15297
|
+
}
|
|
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 ? `
|
|
15058
15392
|
\u26A0\uFE0F Console errors:
|
|
15059
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
|
+
};
|
|
15060
15506
|
}
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
15067
|
-
{
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
|
|
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
|
+
}
|
|
15071
15535
|
}
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
|
|
15076
|
-
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15081
|
-
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15082
|
-
},
|
|
15083
|
-
async ({ code, framework, width, height }) => {
|
|
15084
|
-
try {
|
|
15085
|
-
const result = await auditA11y(code, framework, { width, height });
|
|
15086
|
-
if (result.violations.length === 0) {
|
|
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) {
|
|
15087
15544
|
return {
|
|
15088
15545
|
content: [
|
|
15089
15546
|
{
|
|
15090
15547
|
type: "text",
|
|
15091
|
-
text:
|
|
15548
|
+
text: "No snapshots saved. Use snapshot_save to create one."
|
|
15092
15549
|
}
|
|
15093
15550
|
]
|
|
15094
15551
|
};
|
|
15095
15552
|
}
|
|
15096
|
-
const
|
|
15097
|
-
|
|
15098
|
-
|
|
15099
|
-
return `[${v.impact?.toUpperCase()}] ${v.id}
|
|
15100
|
-
${v.description}
|
|
15101
|
-
${v.helpUrl}
|
|
15102
|
-
${nodes}`;
|
|
15103
|
-
}).join("\n\n");
|
|
15553
|
+
const lines = snapshots.map(
|
|
15554
|
+
(s) => `\u2022 ${s.key} (saved ${new Date(s.timestamp).toLocaleTimeString()})`
|
|
15555
|
+
);
|
|
15104
15556
|
return {
|
|
15105
15557
|
content: [
|
|
15106
15558
|
{
|
|
15107
15559
|
type: "text",
|
|
15108
|
-
text: `
|
|
15109
|
-
|
|
15110
|
-
${report}
|
|
15111
|
-
|
|
15112
|
-
(${result.passes} rules passed, ${result.incomplete} need review)`
|
|
15560
|
+
text: `Saved snapshots (${snapshots.length}):
|
|
15561
|
+
${lines.join("\n")}`
|
|
15113
15562
|
}
|
|
15114
15563
|
]
|
|
15115
15564
|
};
|
|
15116
|
-
} catch (error51) {
|
|
15117
|
-
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15118
|
-
return {
|
|
15119
|
-
content: [{ type: "text", text: `A11y audit failed: ${msg}` }],
|
|
15120
|
-
isError: true
|
|
15121
|
-
};
|
|
15122
15565
|
}
|
|
15123
|
-
|
|
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
|
|
15124
15588
|
);
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
|
|
15132
|
-
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15133
|
-
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15134
|
-
},
|
|
15135
|
-
async ({ before, after, framework, width, height }) => {
|
|
15136
|
-
try {
|
|
15137
|
-
const result = await diffComponent(before, after, framework, {
|
|
15138
|
-
width,
|
|
15139
|
-
height
|
|
15140
|
-
});
|
|
15141
|
-
const content = [
|
|
15142
|
-
{
|
|
15143
|
-
type: "text",
|
|
15144
|
-
text: `Visual diff: ${result.diffPercentage}% pixels changed (${result.diffPixels}/${result.totalPixels})`
|
|
15145
|
-
},
|
|
15146
|
-
{ type: "text", text: "Before:" },
|
|
15147
|
-
{
|
|
15148
|
-
type: "image",
|
|
15149
|
-
data: result.before,
|
|
15150
|
-
mimeType: "image/png"
|
|
15151
|
-
},
|
|
15152
|
-
{ type: "text", text: "After:" },
|
|
15153
|
-
{
|
|
15154
|
-
type: "image",
|
|
15155
|
-
data: result.after,
|
|
15156
|
-
mimeType: "image/png"
|
|
15157
|
-
},
|
|
15158
|
-
{ type: "text", text: "Diff (red = changed pixels):" },
|
|
15159
|
-
{
|
|
15160
|
-
type: "image",
|
|
15161
|
-
data: result.diff,
|
|
15162
|
-
mimeType: "image/png"
|
|
15163
|
-
}
|
|
15164
|
-
];
|
|
15165
|
-
return { content };
|
|
15166
|
-
} catch (error51) {
|
|
15167
|
-
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15168
|
-
return {
|
|
15169
|
-
content: [{ type: "text", text: `Diff failed: ${msg}` }],
|
|
15170
|
-
isError: true
|
|
15171
|
-
};
|
|
15172
|
-
}
|
|
15173
|
-
}
|
|
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
|
|
15174
15596
|
);
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
15181
|
-
frames: external_exports.number().optional().default(5).describe("Number of frames to capture"),
|
|
15182
|
-
duration: external_exports.number().optional().default(1e3).describe("Total capture duration in ms"),
|
|
15183
|
-
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15184
|
-
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15185
|
-
},
|
|
15186
|
-
async ({ code, framework, frames, duration: duration3, width, height }) => {
|
|
15597
|
+
var catalogUseCase = new CatalogUseCase(renderUseCase);
|
|
15598
|
+
async function ensureBrowser() {
|
|
15599
|
+
try {
|
|
15600
|
+
await browserPool.warmup(["chromium"]);
|
|
15601
|
+
} catch {
|
|
15187
15602
|
try {
|
|
15188
|
-
|
|
15189
|
-
|
|
15190
|
-
|
|
15191
|
-
width,
|
|
15192
|
-
height
|
|
15193
|
-
});
|
|
15194
|
-
const content = results.flatMap((r) => [
|
|
15195
|
-
{
|
|
15196
|
-
type: "image",
|
|
15197
|
-
data: r.image,
|
|
15198
|
-
mimeType: "image/png"
|
|
15199
|
-
},
|
|
15200
|
-
{
|
|
15201
|
-
type: "text",
|
|
15202
|
-
text: `[${r.timestamp}ms]`
|
|
15203
|
-
}
|
|
15204
|
-
]);
|
|
15205
|
-
return { content };
|
|
15206
|
-
} catch (error51) {
|
|
15207
|
-
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15208
|
-
return {
|
|
15209
|
-
content: [
|
|
15210
|
-
{ type: "text", text: `Animation capture failed: ${msg}` }
|
|
15211
|
-
],
|
|
15212
|
-
isError: true
|
|
15213
|
-
};
|
|
15603
|
+
execSync("npx playwright install chromium", { stdio: "pipe" });
|
|
15604
|
+
await browserPool.warmup(["chromium"]);
|
|
15605
|
+
} catch {
|
|
15214
15606
|
}
|
|
15215
15607
|
}
|
|
15216
|
-
|
|
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
|
|
15621
|
+
});
|
|
15217
15622
|
var transport = new StdioServerTransport();
|
|
15218
15623
|
await server.connect(transport);
|
|
15219
15624
|
process.on("SIGINT", async () => {
|
|
15220
|
-
await shutdown();
|
|
15625
|
+
await browserPool.shutdown();
|
|
15221
15626
|
process.exit(0);
|
|
15222
15627
|
});
|
|
15223
15628
|
process.on("SIGTERM", async () => {
|
|
15224
|
-
await shutdown();
|
|
15629
|
+
await browserPool.shutdown();
|
|
15225
15630
|
process.exit(0);
|
|
15226
15631
|
});
|