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/dist/index.js CHANGED
@@ -1,11 +1,22 @@
1
1
  #!/usr/bin/env node
2
- var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
4
- for (var name in all)
5
- __defProp(target, name, { get: all[name], enumerable: true });
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/renderer.ts
14527
- import { chromium, firefox, webkit } from "playwright";
14528
- var pool = /* @__PURE__ */ new Map();
14529
- var ENGINES = ["chromium", "firefox", "webkit"];
14530
- async function warmup(engines = ENGINES) {
14531
- await Promise.all(engines.map((e) => getSlot(e)));
14532
- }
14533
- async function getSlot(engine) {
14534
- const existing = pool.get(engine);
14535
- if (existing?.browser.isConnected() && existing.ready) {
14536
- return existing;
14537
- }
14538
- const launcher = { chromium, firefox, webkit }[engine];
14539
- let browser;
14540
- try {
14541
- browser = await launcher.launch({
14542
- headless: true,
14543
- ...engine === "chromium" ? { channel: "chrome" } : {}
14544
- });
14545
- } catch (e) {
14546
- throw new Error(`${engine} is not installed. Run: npx playwright install ${engine}`);
14547
- }
14548
- const context = await browser.newContext({
14549
- viewport: { width: 1280, height: 800 },
14550
- deviceScaleFactor: 2
14551
- });
14552
- const page = await context.newPage();
14553
- await page.setContent(
14554
- '<html><head><script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script></head><body></body></html>',
14555
- { waitUntil: "networkidle" }
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
- const slot = { browser, page, ready: true };
14558
- pool.set(engine, slot);
14559
- return slot;
14560
- }
14561
- function wrapComponent(code, framework) {
14562
- const tailwind = '<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>';
14563
- const baseStyle = `<style>*{margin:0;box-sizing:border-box}body{padding:16px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}</style>`;
14564
- if (framework === "html") {
14565
- if (code.includes("<html")) return code;
14566
- return `<!DOCTYPE html><html><head><meta charset="utf-8">${tailwind}${baseStyle}</head><body>${code}</body></html>`;
14567
- }
14568
- if (framework === "react") {
14569
- return `<!DOCTYPE html><html><head><meta charset="utf-8">${tailwind}
14570
- <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
14571
- <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
14572
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
14573
- ${baseStyle}</head><body><div id="root"></div>
14574
- <script type="text/babel">
14575
- ${code}
14576
- const _C = typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:typeof Default!=='undefined'?Default:null;
14577
- if(_C)ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(_C));
14578
- </script></body></html>`;
14579
- }
14580
- if (framework === "vue") {
14581
- return `<!DOCTYPE html><html><head><meta charset="utf-8">${tailwind}
14582
- <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
14583
- ${baseStyle}</head><body><div id="app"></div>
14584
- <script>
14585
- const{createApp,ref,reactive,computed,onMounted,watch,watchEffect}=Vue;
14586
- ${code}
14587
- const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
14588
- if(_C)createApp(_C).mount('#app');
14589
- </script></body></html>`;
14590
- }
14591
- return code;
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
- return results;
14629
- }
14630
- async function screenshotUrl(url2, options = {}) {
14631
- const engines = options.engines ?? ["chromium"];
14632
- const results = await Promise.all(
14633
- engines.map(async (engine) => {
14634
- const { width = 1280, height = 800, fullPage = true, waitFor = 0 } = options;
14635
- const slot = await getSlot(engine);
14636
- const { page } = slot;
14637
- const currentViewport = page.viewportSize();
14638
- if (currentViewport?.width !== width || currentViewport?.height !== height) {
14639
- await page.setViewportSize({ width, height });
14640
- }
14641
- await page.goto(url2, { waitUntil: "domcontentloaded", timeout: 15e3 });
14642
- await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
14643
- });
14644
- if (waitFor > 0) {
14645
- await page.waitForTimeout(waitFor);
14646
- }
14647
- const screenshot = await page.screenshot({ type: "png", fullPage });
14648
- const metrics = await page.evaluate(() => ({
14649
- w: document.documentElement.scrollWidth,
14650
- h: document.documentElement.scrollHeight
14651
- }));
14652
- return { engine, image: screenshot.toString("base64"), width: metrics.w, height: metrics.h };
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
- async function shutdown() {
14658
- for (const slot of pool.values()) {
14659
- await slot.browser.close().catch(() => {
14660
- });
14661
- }
14662
- pool.clear();
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/index.ts
14666
- var server = new McpServer({
14667
- name: "frameshot",
14668
- version: "0.1.0"
14669
- });
14670
- server.tool(
14671
- "render_component",
14672
- "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.",
14673
- {
14674
- code: external_exports.string().describe("Component code to render"),
14675
- framework: external_exports.enum(["html", "react", "vue"]).default("react").describe("Framework: html, react, or vue"),
14676
- engines: external_exports.array(external_exports.enum(["chromium", "firefox", "webkit"])).optional().default(["chromium"]).describe('Browser engines to render in. Use ["chromium","firefox","webkit"] for cross-browser check.'),
14677
- width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
14678
- height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
14679
- fullPage: external_exports.boolean().optional().default(true).describe("Capture full scroll height")
14680
- },
14681
- async ({ code, framework, engines, width, height, fullPage }) => {
14682
- try {
14683
- const results = await render(code, framework, {
14684
- width,
14685
- height,
14686
- fullPage,
14687
- engines
14688
- });
14689
- const content = results.flatMap((r) => [
14690
- {
14691
- type: "image",
14692
- data: r.image,
14693
- mimeType: "image/png"
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: `[${r.engine}] ${r.width}x${r.height}`
14698
- }
14699
- ]);
14700
- return { content };
14701
- } catch (error51) {
14702
- const msg = error51 instanceof Error ? error51.message : String(error51);
14703
- return { content: [{ type: "text", text: `Render failed: ${msg}` }], isError: true };
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
- server.tool(
14708
- "screenshot_url",
14709
- "Take a screenshot of a URL (e.g. localhost:3000) across multiple browser engines in parallel.",
14710
- {
14711
- url: external_exports.string().url().describe("URL to screenshot"),
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
- const results = await screenshotUrl(url2, {
14720
- width,
14721
- height,
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
- var transport = new StdioServerTransport();
14744
- warmup(["chromium"]).catch(() => {
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
  });