frameshot-mcp 0.7.0 → 0.9.7

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.
Files changed (67) hide show
  1. package/README.md +83 -69
  2. package/action.yml +114 -16
  3. package/dist/chunk-MEBQ7ZWA.js +1774 -0
  4. package/dist/chunk-VUYZHZBH.js +157 -0
  5. package/dist/cli.js +131 -133
  6. package/dist/index.js +519 -572
  7. package/dist/renderer.d.ts +17 -7
  8. package/dist/renderer.js +4 -6
  9. package/dist/stubs/gatsby.js +20 -0
  10. package/dist/stubs/next-font.js +9 -0
  11. package/dist/stubs/next-headers.js +20 -0
  12. package/dist/stubs/next-image.js +34 -0
  13. package/dist/stubs/next-link.js +25 -0
  14. package/dist/stubs/next-navigation.js +17 -0
  15. package/dist/stubs/next-router.js +19 -0
  16. package/dist/stubs/nuxt-app.js +37 -0
  17. package/dist/stubs/nuxt-imports.js +13 -0
  18. package/dist/stubs/qwik-city.js +33 -0
  19. package/dist/stubs/react-router.js +67 -0
  20. package/dist/stubs/server-only.js +2 -0
  21. package/dist/stubs/solid-router.js +27 -0
  22. package/dist/stubs/solid-start.js +18 -0
  23. package/dist/stubs/stubs/gatsby.js +20 -0
  24. package/dist/stubs/stubs/next-font.js +9 -0
  25. package/dist/stubs/stubs/next-headers.js +20 -0
  26. package/dist/stubs/stubs/next-image.js +34 -0
  27. package/dist/stubs/stubs/next-link.js +25 -0
  28. package/dist/stubs/stubs/next-navigation.js +17 -0
  29. package/dist/stubs/stubs/next-router.js +19 -0
  30. package/dist/stubs/stubs/nuxt-app.js +37 -0
  31. package/dist/stubs/stubs/nuxt-imports.js +13 -0
  32. package/dist/stubs/stubs/qwik-city.js +33 -0
  33. package/dist/stubs/stubs/react-router.js +67 -0
  34. package/dist/stubs/stubs/server-only.js +2 -0
  35. package/dist/stubs/stubs/solid-router.js +27 -0
  36. package/dist/stubs/stubs/solid-start.js +18 -0
  37. package/dist/stubs/stubs/sveltekit-environment.js +5 -0
  38. package/dist/stubs/stubs/sveltekit-navigation.js +11 -0
  39. package/dist/stubs/stubs/sveltekit-stores.js +15 -0
  40. package/dist/stubs/stubs/vike.js +11 -0
  41. package/dist/stubs/sveltekit-environment.js +5 -0
  42. package/dist/stubs/sveltekit-navigation.js +11 -0
  43. package/dist/stubs/sveltekit-stores.js +15 -0
  44. package/dist/stubs/vike.js +11 -0
  45. package/package.json +10 -4
  46. package/scripts/render-changed.mjs +140 -18
  47. package/dist/chunk-3LVWVDET.js +0 -849
  48. package/dist/chunk-47YJG5HR.js +0 -690
  49. package/dist/chunk-67JZQ6OI.js +0 -819
  50. package/dist/chunk-AZCGKIMU.js +0 -850
  51. package/dist/chunk-B3CLIGWU.js +0 -786
  52. package/dist/chunk-C6QSY4WR.js +0 -811
  53. package/dist/chunk-DX54PJKO.js +0 -603
  54. package/dist/chunk-EMCJGIMY.js +0 -984
  55. package/dist/chunk-FQNWGR62.js +0 -849
  56. package/dist/chunk-FTYTZW6D.js +0 -203
  57. package/dist/chunk-JGVKYXY2.js +0 -857
  58. package/dist/chunk-JYPEA4P2.js +0 -846
  59. package/dist/chunk-KHK35HDD.js +0 -855
  60. package/dist/chunk-Q7A3DLED.js +0 -848
  61. package/dist/chunk-SIA6XEHM.js +0 -811
  62. package/dist/chunk-ST35YDI6.js +0 -834
  63. package/dist/chunk-T5OBJK35.js +0 -855
  64. package/dist/chunk-U3GHS7KO.js +0 -837
  65. package/dist/chunk-WS2ASCD6.js +0 -683
  66. package/dist/chunk-WZMHVSUA.js +0 -847
  67. package/dist/chunk-ZZST6K7Y.js +0 -987
package/dist/index.js CHANGED
@@ -1,22 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- AuditUseCase,
4
- ScreenshotUseCase,
5
- SnapshotStore,
6
- SnapshotUseCase
7
- } from "./chunk-FTYTZW6D.js";
3
+ createContainer
4
+ } from "./chunk-VUYZHZBH.js";
8
5
  import {
9
- BrowserPool,
10
- CatalogUseCase,
11
6
  DEVICE_PRESETS,
12
- DiffUseCase,
13
7
  EXT_TO_FRAMEWORK,
14
- HtmlBuilder,
15
- ImageComparator,
16
- RenderUseCase,
17
- ViteBundler,
18
8
  __export
19
- } from "./chunk-Q7A3DLED.js";
9
+ } from "./chunk-MEBQ7ZWA.js";
20
10
 
21
11
  // src/index.ts
22
12
  import { execSync } from "child_process";
@@ -14537,6 +14527,25 @@ function date4(params) {
14537
14527
  // node_modules/zod/v4/classic/external.js
14538
14528
  config(en_default());
14539
14529
 
14530
+ // src/tools/tool-utils.ts
14531
+ function wrapHandler(handler) {
14532
+ return async (args) => {
14533
+ try {
14534
+ return await handler(args);
14535
+ } catch (e) {
14536
+ return {
14537
+ content: [
14538
+ {
14539
+ type: "text",
14540
+ text: `Error: ${e instanceof Error ? e.message : String(e)}`
14541
+ }
14542
+ ],
14543
+ isError: true
14544
+ };
14545
+ }
14546
+ };
14547
+ }
14548
+
14540
14549
  // src/tools/audit-tools.ts
14541
14550
  function registerAuditTools(server2, useCase) {
14542
14551
  server2.tool(
@@ -14548,51 +14557,41 @@ function registerAuditTools(server2, useCase) {
14548
14557
  width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
14549
14558
  height: external_exports.number().optional().default(800).describe("Viewport height (px)")
14550
14559
  },
14551
- async ({ code, framework, width, height }) => {
14552
- try {
14553
- const result = await useCase.auditA11y(code, framework, {
14554
- viewport: { width, height }
14555
- });
14556
- if (result.violations.length === 0) {
14557
- return {
14558
- content: [
14559
- {
14560
- type: "text",
14561
- text: `\u2705 No accessibility violations found. (${result.passes} rules passed, ${result.incomplete} need review)`
14562
- }
14563
- ]
14564
- };
14565
- }
14566
- const report = result.violations.map((v) => {
14567
- const nodes = v.nodes.slice(0, 3).map((n) => ` ${n.target.join(" > ")}
14568
- ${n.html}`).join("\n");
14569
- return `[${v.impact?.toUpperCase()}] ${v.id}
14570
- ${v.description}
14571
- ${v.helpUrl}
14572
- ${nodes}`;
14573
- }).join("\n\n");
14560
+ wrapHandler(async ({ code, framework, width, height }) => {
14561
+ const result = await useCase.auditA11y(code, framework, {
14562
+ viewport: { width, height }
14563
+ });
14564
+ if (result.violations.length === 0) {
14574
14565
  return {
14575
14566
  content: [
14576
14567
  {
14577
14568
  type: "text",
14578
- text: `Found ${result.violations.length} accessibility violation(s):
14579
-
14580
- ${report}
14581
-
14582
- (${result.passes} rules passed, ${result.incomplete} need review)`
14569
+ text: `\u2705 No accessibility violations found. (${result.passes} rules passed, ${result.incomplete} need review)`
14583
14570
  }
14584
14571
  ]
14585
14572
  };
14586
- } catch (error51) {
14587
- const msg = error51 instanceof Error ? error51.message : String(error51);
14588
- return {
14589
- content: [
14590
- { type: "text", text: `A11y audit failed: ${msg}` }
14591
- ],
14592
- isError: true
14593
- };
14594
14573
  }
14595
- }
14574
+ const report = result.violations.map((v) => {
14575
+ const nodes = v.nodes.slice(0, 3).map((n) => ` ${n.target.join(" > ")}
14576
+ ${n.html}`).join("\n");
14577
+ return `[${v.impact?.toUpperCase()}] ${v.id}
14578
+ ${v.description}
14579
+ ${v.helpUrl}
14580
+ ${nodes}`;
14581
+ }).join("\n\n");
14582
+ return {
14583
+ content: [
14584
+ {
14585
+ type: "text",
14586
+ text: `Found ${result.violations.length} accessibility violation(s):
14587
+
14588
+ ${report}
14589
+
14590
+ (${result.passes} rules passed, ${result.incomplete} need review)`
14591
+ }
14592
+ ]
14593
+ };
14594
+ })
14596
14595
  );
14597
14596
  server2.tool(
14598
14597
  "perf_audit",
@@ -14603,33 +14602,23 @@ ${report}
14603
14602
  width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
14604
14603
  height: external_exports.number().optional().default(800).describe("Viewport height (px)")
14605
14604
  },
14606
- async ({ code, framework, width, height }) => {
14607
- try {
14608
- const metrics = await useCase.perfAudit(code, framework, {
14609
- viewport: { width, height }
14610
- });
14611
- const lines = [
14612
- `\u23F1\uFE0F Render time: ${metrics.renderTimeMs}ms`,
14613
- `\u{1F4E6} DOM elements: ${metrics.domElements}`,
14614
- `\u{1F333} DOM depth: ${metrics.domDepth}`,
14615
- `\u{1F4DC} Scripts: ${metrics.scriptCount}`,
14616
- `\u{1F3A8} Stylesheets: ${metrics.styleSheetCount}`,
14617
- `\u{1F5BC}\uFE0F Images: ${metrics.imageCount}`,
14618
- `\u{1F4D0} Total DOM size: ${(metrics.totalDomSize / 1024).toFixed(1)}KB`
14619
- ];
14620
- return {
14621
- content: [{ type: "text", text: lines.join("\n") }]
14622
- };
14623
- } catch (error51) {
14624
- const msg = error51 instanceof Error ? error51.message : String(error51);
14625
- return {
14626
- content: [
14627
- { type: "text", text: `Perf audit failed: ${msg}` }
14628
- ],
14629
- isError: true
14630
- };
14631
- }
14632
- }
14605
+ wrapHandler(async ({ code, framework, width, height }) => {
14606
+ const metrics = await useCase.perfAudit(code, framework, {
14607
+ viewport: { width, height }
14608
+ });
14609
+ const lines = [
14610
+ `\u23F1\uFE0F Render time: ${metrics.renderTimeMs}ms`,
14611
+ `\u{1F4E6} DOM elements: ${metrics.domElements}`,
14612
+ `\u{1F333} DOM depth: ${metrics.domDepth}`,
14613
+ `\u{1F4DC} Scripts: ${metrics.scriptCount}`,
14614
+ `\u{1F3A8} Stylesheets: ${metrics.styleSheetCount}`,
14615
+ `\u{1F5BC}\uFE0F Images: ${metrics.imageCount}`,
14616
+ `\u{1F4D0} Total DOM size: ${(metrics.totalDomSize / 1024).toFixed(1)}KB`
14617
+ ];
14618
+ return {
14619
+ content: [{ type: "text", text: lines.join("\n") }]
14620
+ };
14621
+ })
14633
14622
  );
14634
14623
  }
14635
14624
 
@@ -14646,15 +14635,15 @@ function registerCatalogTools(server2, useCase) {
14646
14635
  darkMode: external_exports.boolean().optional().default(false).describe("Render with dark mode"),
14647
14636
  tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
14648
14637
  },
14649
- async ({
14650
- directory,
14651
- recursive,
14652
- width,
14653
- height,
14654
- darkMode,
14655
- tailwindVersion
14656
- }) => {
14657
- try {
14638
+ wrapHandler(
14639
+ async ({
14640
+ directory,
14641
+ recursive,
14642
+ width,
14643
+ height,
14644
+ darkMode,
14645
+ tailwindVersion
14646
+ }) => {
14658
14647
  const results = await useCase.renderCatalog(directory, {
14659
14648
  recursive,
14660
14649
  viewport: { width, height },
@@ -14698,16 +14687,8 @@ function registerCatalogTools(server2, useCase) {
14698
14687
  text: `\u{1F4E6} Component catalog: ${results.length} files from ${directory}`
14699
14688
  });
14700
14689
  return { content };
14701
- } catch (error51) {
14702
- const msg = error51 instanceof Error ? error51.message : String(error51);
14703
- return {
14704
- content: [
14705
- { type: "text", text: `Catalog render failed: ${msg}` }
14706
- ],
14707
- isError: true
14708
- };
14709
14690
  }
14710
- }
14691
+ )
14711
14692
  );
14712
14693
  }
14713
14694
 
@@ -14723,44 +14704,36 @@ function registerDiffTools(server2, useCase) {
14723
14704
  width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
14724
14705
  height: external_exports.number().optional().default(800).describe("Viewport height (px)")
14725
14706
  },
14726
- async ({ before, after, framework, width, height }) => {
14727
- try {
14728
- const result = await useCase.diffComponent(before, after, framework, {
14729
- viewport: { width, height }
14730
- });
14731
- const content = [
14732
- {
14733
- type: "text",
14734
- text: `Visual diff: ${result.diffPercentage}% pixels changed (${result.diffPixels}/${result.totalPixels})`
14735
- },
14736
- { type: "text", text: "Before:" },
14737
- {
14738
- type: "image",
14739
- data: result.before,
14740
- mimeType: "image/png"
14741
- },
14742
- { type: "text", text: "After:" },
14743
- {
14744
- type: "image",
14745
- data: result.after,
14746
- mimeType: "image/png"
14747
- },
14748
- { type: "text", text: "Diff (red = changed pixels):" },
14749
- {
14750
- type: "image",
14751
- data: result.diff,
14752
- mimeType: "image/png"
14753
- }
14754
- ];
14755
- return { content };
14756
- } catch (error51) {
14757
- const msg = error51 instanceof Error ? error51.message : String(error51);
14758
- return {
14759
- content: [{ type: "text", text: `Diff failed: ${msg}` }],
14760
- isError: true
14761
- };
14762
- }
14763
- }
14707
+ wrapHandler(async ({ before, after, framework, width, height }) => {
14708
+ const result = await useCase.diffComponent(before, after, framework, {
14709
+ viewport: { width, height }
14710
+ });
14711
+ const content = [
14712
+ {
14713
+ type: "text",
14714
+ text: `Visual diff: ${result.diffPercentage}% pixels changed (${result.diffPixels}/${result.totalPixels})`
14715
+ },
14716
+ { type: "text", text: "Before:" },
14717
+ {
14718
+ type: "image",
14719
+ data: result.before,
14720
+ mimeType: "image/png"
14721
+ },
14722
+ { type: "text", text: "After:" },
14723
+ {
14724
+ type: "image",
14725
+ data: result.after,
14726
+ mimeType: "image/png"
14727
+ },
14728
+ { type: "text", text: "Diff (red = changed pixels):" },
14729
+ {
14730
+ type: "image",
14731
+ data: result.diff,
14732
+ mimeType: "image/png"
14733
+ }
14734
+ ];
14735
+ return { content };
14736
+ })
14764
14737
  );
14765
14738
  server2.tool(
14766
14739
  "diff_reference",
@@ -14777,17 +14750,17 @@ function registerDiffTools(server2, useCase) {
14777
14750
  darkMode: external_exports.boolean().optional().default(false).describe("Render with dark mode"),
14778
14751
  tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
14779
14752
  },
14780
- async ({
14781
- code,
14782
- framework,
14783
- referenceImage,
14784
- width,
14785
- height,
14786
- threshold,
14787
- darkMode,
14788
- tailwindVersion
14789
- }) => {
14790
- try {
14753
+ wrapHandler(
14754
+ async ({
14755
+ code,
14756
+ framework,
14757
+ referenceImage,
14758
+ width,
14759
+ height,
14760
+ threshold,
14761
+ darkMode,
14762
+ tailwindVersion
14763
+ }) => {
14791
14764
  const result = await useCase.diffFromReference(
14792
14765
  code,
14793
14766
  framework,
@@ -14819,22 +14792,91 @@ function registerDiffTools(server2, useCase) {
14819
14792
  }
14820
14793
  ];
14821
14794
  return { content };
14822
- } catch (error51) {
14823
- const msg = error51 instanceof Error ? error51.message : String(error51);
14824
- return {
14825
- content: [
14826
- { type: "text", text: `Reference diff failed: ${msg}` }
14827
- ],
14828
- isError: true
14829
- };
14830
14795
  }
14831
- }
14796
+ )
14832
14797
  );
14833
14798
  }
14834
14799
 
14835
- // src/tools/render-tools.ts
14800
+ // src/tools/render-animation-tools.ts
14801
+ function registerRenderAnimationTools(server2, useCase) {
14802
+ server2.tool(
14803
+ "capture_animation",
14804
+ "Capture multiple frames of a CSS animation or transition over time. Returns sequential screenshots to verify animation behavior, timing, and smoothness.",
14805
+ {
14806
+ code: external_exports.string().describe("Component code with CSS animations/transitions"),
14807
+ framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
14808
+ frames: external_exports.number().optional().default(5).describe("Number of frames to capture"),
14809
+ duration: external_exports.number().optional().default(1e3).describe("Total capture duration in ms"),
14810
+ width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
14811
+ height: external_exports.number().optional().default(800).describe("Viewport height (px)")
14812
+ },
14813
+ wrapHandler(
14814
+ async ({ code, framework, frames, duration: duration3, width, height }) => {
14815
+ const results = await useCase.captureAnimation(code, framework, {
14816
+ frames,
14817
+ duration: duration3,
14818
+ viewport: { width, height }
14819
+ });
14820
+ const content = results.flatMap((r) => [
14821
+ {
14822
+ type: "image",
14823
+ data: r.image,
14824
+ mimeType: "image/png"
14825
+ },
14826
+ {
14827
+ type: "text",
14828
+ text: `[${r.timestamp}ms]`
14829
+ }
14830
+ ]);
14831
+ return { content };
14832
+ }
14833
+ )
14834
+ );
14835
+ server2.tool(
14836
+ "render_interaction",
14837
+ "Render a component, simulate user interactions (click, hover, focus, type), then screenshot the result. Use this to verify hover states, dropdowns, modals, form inputs.",
14838
+ {
14839
+ code: external_exports.string().describe("Component code to render"),
14840
+ framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
14841
+ interactions: external_exports.array(
14842
+ external_exports.object({
14843
+ action: external_exports.enum(["click", "hover", "focus", "type", "wait"]).describe("Interaction type"),
14844
+ selector: external_exports.string().optional().describe("CSS selector for the target element"),
14845
+ value: external_exports.string().optional().describe("Text to type (for 'type' action)"),
14846
+ ms: external_exports.number().optional().describe("Wait duration in ms (for 'wait' action)")
14847
+ })
14848
+ ).describe("Sequence of interactions to perform"),
14849
+ width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
14850
+ height: external_exports.number().optional().default(800).describe("Viewport height (px)")
14851
+ },
14852
+ wrapHandler(async ({ code, framework, interactions, width, height }) => {
14853
+ const result = await useCase.renderInteraction(
14854
+ code,
14855
+ framework,
14856
+ interactions,
14857
+ {
14858
+ viewport: { width, height }
14859
+ }
14860
+ );
14861
+ const content = [
14862
+ {
14863
+ type: "image",
14864
+ data: result.image,
14865
+ mimeType: "image/png"
14866
+ },
14867
+ {
14868
+ type: "text",
14869
+ text: `After ${interactions.length} interaction(s) \u2014 ${result.width}x${result.height}`
14870
+ }
14871
+ ];
14872
+ return { content };
14873
+ })
14874
+ );
14875
+ }
14876
+
14877
+ // src/tools/render-file-tools.ts
14836
14878
  import { extname } from "path";
14837
- function registerRenderTools(server2, useCase) {
14879
+ function registerRenderFileTools(server2, useCase) {
14838
14880
  server2.tool(
14839
14881
  "render_file",
14840
14882
  "Render a component file with full dependency resolution. Uses your project's Vite config to resolve imports, path aliases, CSS modules, and node_modules. Falls back to CDN-based rendering if Vite is unavailable. Auto-detects framework from file extension.",
@@ -14849,16 +14891,16 @@ function registerRenderTools(server2, useCase) {
14849
14891
  "Project root directory override (defaults to auto-detect from file path)"
14850
14892
  )
14851
14893
  },
14852
- async ({
14853
- path,
14854
- props,
14855
- width,
14856
- height,
14857
- darkMode,
14858
- tailwindVersion,
14859
- projectRoot
14860
- }) => {
14861
- try {
14894
+ wrapHandler(
14895
+ async ({
14896
+ path,
14897
+ props,
14898
+ width,
14899
+ height,
14900
+ darkMode,
14901
+ tailwindVersion,
14902
+ projectRoot
14903
+ }) => {
14862
14904
  const start = performance.now();
14863
14905
  const { results, mode } = await useCase.renderFile(path, {
14864
14906
  props,
@@ -14886,16 +14928,8 @@ ${r.consoleErrors.join("\n")}` : ""}`
14886
14928
  }
14887
14929
  ]);
14888
14930
  return { content };
14889
- } catch (error51) {
14890
- const msg = error51 instanceof Error ? error51.message : String(error51);
14891
- return {
14892
- content: [
14893
- { type: "text", text: `File render failed: ${msg}` }
14894
- ],
14895
- isError: true
14896
- };
14897
14931
  }
14898
- }
14932
+ )
14899
14933
  );
14900
14934
  server2.tool(
14901
14935
  "render_component",
@@ -14916,19 +14950,19 @@ ${r.consoleErrors.join("\n")}` : ""}`
14916
14950
  css: external_exports.string().optional().describe("Custom CSS to inject (design tokens, variables, etc)"),
14917
14951
  tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
14918
14952
  },
14919
- async ({
14920
- code,
14921
- framework,
14922
- engines,
14923
- width,
14924
- height,
14925
- fullPage,
14926
- darkMode,
14927
- colorSchemes,
14928
- css,
14929
- tailwindVersion
14930
- }) => {
14931
- try {
14953
+ wrapHandler(
14954
+ async ({
14955
+ code,
14956
+ framework,
14957
+ engines,
14958
+ width,
14959
+ height,
14960
+ fullPage,
14961
+ darkMode,
14962
+ colorSchemes,
14963
+ css,
14964
+ tailwindVersion
14965
+ }) => {
14932
14966
  const start = performance.now();
14933
14967
  const schemes = colorSchemes ?? (darkMode ? ["dark"] : ["light"]);
14934
14968
  const allResults = await Promise.all(
@@ -14959,15 +14993,19 @@ ${r.consoleErrors.join("\n")}` : ""}`
14959
14993
  }
14960
14994
  ]);
14961
14995
  return { content };
14962
- } catch (error51) {
14963
- const msg = error51 instanceof Error ? error51.message : String(error51);
14964
- return {
14965
- content: [{ type: "text", text: `Render failed: ${msg}` }],
14966
- isError: true
14967
- };
14968
14996
  }
14969
- }
14997
+ )
14970
14998
  );
14999
+ }
15000
+
15001
+ // src/tools/render-visual-tools.ts
15002
+ function imageTextContent(image, text) {
15003
+ return [
15004
+ { type: "image", data: image, mimeType: "image/png" },
15005
+ { type: "text", text }
15006
+ ];
15007
+ }
15008
+ function registerRenderVisualTools(server2, useCase) {
14971
15009
  server2.tool(
14972
15010
  "render_responsive",
14973
15011
  "Render a component at mobile, tablet, and desktop sizes in one call. Returns 3 screenshots for responsive verification.",
@@ -14976,44 +15014,70 @@ ${r.consoleErrors.join("\n")}` : ""}`
14976
15014
  framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
14977
15015
  devices: external_exports.array(external_exports.enum(["mobile", "tablet", "desktop"])).optional().default(["mobile", "tablet", "desktop"]).describe("Device sizes to render")
14978
15016
  },
14979
- async ({ code, framework, devices }) => {
14980
- try {
14981
- const results = await Promise.all(
14982
- devices.map(async (device) => {
14983
- const preset = DEVICE_PRESETS[device];
14984
- const [result] = await useCase.render(code, framework, {
14985
- viewport: { width: preset.width, height: preset.height },
14986
- fullPage: true,
14987
- engines: ["chromium"]
14988
- });
14989
- return { device, ...result };
14990
- })
14991
- );
14992
- const content = results.flatMap((r) => [
14993
- {
14994
- type: "image",
14995
- data: r.image,
14996
- mimeType: "image/png"
14997
- },
14998
- {
14999
- type: "text",
15000
- text: `[${r.device}] ${r.width}x${r.height}`
15001
- }
15002
- ]);
15003
- return { content };
15004
- } catch (error51) {
15005
- const msg = error51 instanceof Error ? error51.message : String(error51);
15006
- return {
15007
- content: [
15008
- {
15009
- type: "text",
15010
- text: `Responsive render failed: ${msg}`
15011
- }
15012
- ],
15013
- isError: true
15014
- };
15015
- }
15016
- }
15017
+ wrapHandler(async ({ code, framework, devices }) => {
15018
+ const results = await Promise.all(
15019
+ devices.map(async (device) => {
15020
+ const preset = DEVICE_PRESETS[device];
15021
+ const [result] = await useCase.render(code, framework, {
15022
+ viewport: { width: preset.width, height: preset.height },
15023
+ fullPage: true,
15024
+ engines: ["chromium"]
15025
+ });
15026
+ return { device, ...result };
15027
+ })
15028
+ );
15029
+ const content = results.flatMap(
15030
+ (r) => imageTextContent(r.image, `[${r.device}] ${r.width}x${r.height}`)
15031
+ );
15032
+ return { content };
15033
+ })
15034
+ );
15035
+ server2.tool(
15036
+ "render_theme",
15037
+ "Render a component in both light and dark mode side-by-side. Returns 2 labeled screenshots for quick theme verification.",
15038
+ {
15039
+ code: external_exports.string().describe("Component code to render"),
15040
+ framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
15041
+ width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
15042
+ height: external_exports.number().optional().default(800).describe("Viewport height (px)")
15043
+ },
15044
+ wrapHandler(async ({ code, framework, width, height }) => {
15045
+ const start = performance.now();
15046
+ const [lightResults, darkResults] = await Promise.all([
15047
+ useCase.render(code, framework, {
15048
+ viewport: { width, height },
15049
+ fullPage: true,
15050
+ engines: ["chromium"],
15051
+ darkMode: false
15052
+ }),
15053
+ useCase.render(code, framework, {
15054
+ viewport: { width, height },
15055
+ fullPage: true,
15056
+ engines: ["chromium"],
15057
+ darkMode: true
15058
+ })
15059
+ ]);
15060
+ const elapsed = Math.round(performance.now() - start);
15061
+ const content = [
15062
+ { type: "text", text: "\u2600\uFE0F Light mode:" },
15063
+ {
15064
+ type: "image",
15065
+ data: lightResults[0].image,
15066
+ mimeType: "image/png"
15067
+ },
15068
+ { type: "text", text: "\u{1F319} Dark mode:" },
15069
+ {
15070
+ type: "image",
15071
+ data: darkResults[0].image,
15072
+ mimeType: "image/png"
15073
+ },
15074
+ {
15075
+ type: "text",
15076
+ text: `${lightResults[0].width}x${lightResults[0].height} (${elapsed}ms)`
15077
+ }
15078
+ ];
15079
+ return { content };
15080
+ })
15017
15081
  );
15018
15082
  server2.tool(
15019
15083
  "render_variants",
@@ -15030,110 +15094,35 @@ ${r.consoleErrors.join("\n")}` : ""}`
15030
15094
  width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
15031
15095
  height: external_exports.number().optional().default(800).describe("Viewport height (px)")
15032
15096
  },
15033
- async ({ code, variants, framework, width, height }) => {
15034
- try {
15035
- const results = await Promise.all(
15036
- variants.map(async (variant) => {
15037
- const wrappedCode = framework === "react" ? `${code}
15097
+ wrapHandler(async ({ code, variants, framework, width, height }) => {
15098
+ const results = await Promise.all(
15099
+ variants.map(async (variant) => {
15100
+ const wrappedCode = framework === "react" ? `${code}
15038
15101
  const _VARIANT_PROPS = ${variant.props};
15039
15102
  function _VariantWrapper() { return <App {..._VARIANT_PROPS} />; }` : code;
15040
- const renderFramework = framework;
15041
- const overrideCode = framework === "react" ? wrappedCode.replace(
15042
- /const _C = typeof App/,
15043
- "const _C = typeof _VariantWrapper!=='undefined'?_VariantWrapper:typeof App"
15044
- ) : wrappedCode;
15045
- const [result] = await useCase.render(
15046
- overrideCode,
15047
- renderFramework,
15048
- {
15049
- viewport: { width, height },
15050
- fullPage: true,
15051
- engines: ["chromium"]
15052
- }
15053
- );
15054
- return { label: variant.label, ...result };
15055
- })
15056
- );
15057
- const content = results.flatMap((r) => [
15058
- {
15059
- type: "image",
15060
- data: r.image,
15061
- mimeType: "image/png"
15062
- },
15063
- {
15064
- type: "text",
15065
- text: `[${r.label}] ${r.width}x${r.height}${r.consoleErrors.length ? `
15103
+ const renderFramework = framework;
15104
+ const overrideCode = framework === "react" ? wrappedCode.replace(
15105
+ /const _C = typeof App/,
15106
+ "const _C = typeof _VariantWrapper!=='undefined'?_VariantWrapper:typeof App"
15107
+ ) : wrappedCode;
15108
+ const [result] = await useCase.render(overrideCode, renderFramework, {
15109
+ viewport: { width, height },
15110
+ fullPage: true,
15111
+ engines: ["chromium"]
15112
+ });
15113
+ return { label: variant.label, ...result };
15114
+ })
15115
+ );
15116
+ const content = results.flatMap(
15117
+ (r) => imageTextContent(
15118
+ r.image,
15119
+ `[${r.label}] ${r.width}x${r.height}${r.consoleErrors.length ? `
15066
15120
  \u26A0\uFE0F Console errors:
15067
15121
  ${r.consoleErrors.join("\n")}` : ""}`
15068
- }
15069
- ]);
15070
- return { content };
15071
- } catch (error51) {
15072
- const msg = error51 instanceof Error ? error51.message : String(error51);
15073
- return {
15074
- content: [
15075
- {
15076
- type: "text",
15077
- text: `Variants render failed: ${msg}`
15078
- }
15079
- ],
15080
- isError: true
15081
- };
15082
- }
15083
- }
15084
- );
15085
- server2.tool(
15086
- "render_interaction",
15087
- "Render a component, simulate user interactions (click, hover, focus, type), then screenshot the result. Use this to verify hover states, dropdowns, modals, form inputs.",
15088
- {
15089
- code: external_exports.string().describe("Component code to render"),
15090
- framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
15091
- interactions: external_exports.array(
15092
- external_exports.object({
15093
- action: external_exports.enum(["click", "hover", "focus", "type", "wait"]).describe("Interaction type"),
15094
- selector: external_exports.string().optional().describe("CSS selector for the target element"),
15095
- value: external_exports.string().optional().describe("Text to type (for 'type' action)"),
15096
- ms: external_exports.number().optional().describe("Wait duration in ms (for 'wait' action)")
15097
- })
15098
- ).describe("Sequence of interactions to perform"),
15099
- width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
15100
- height: external_exports.number().optional().default(800).describe("Viewport height (px)")
15101
- },
15102
- async ({ code, framework, interactions, width, height }) => {
15103
- try {
15104
- const result = await useCase.renderInteraction(
15105
- code,
15106
- framework,
15107
- interactions,
15108
- {
15109
- viewport: { width, height }
15110
- }
15111
- );
15112
- const content = [
15113
- {
15114
- type: "image",
15115
- data: result.image,
15116
- mimeType: "image/png"
15117
- },
15118
- {
15119
- type: "text",
15120
- text: `After ${interactions.length} interaction(s) \u2014 ${result.width}x${result.height}`
15121
- }
15122
- ];
15123
- return { content };
15124
- } catch (error51) {
15125
- const msg = error51 instanceof Error ? error51.message : String(error51);
15126
- return {
15127
- content: [
15128
- {
15129
- type: "text",
15130
- text: `Interaction render failed: ${msg}`
15131
- }
15132
- ],
15133
- isError: true
15134
- };
15135
- }
15136
- }
15122
+ )
15123
+ );
15124
+ return { content };
15125
+ })
15137
15126
  );
15138
15127
  server2.tool(
15139
15128
  "render_grid",
@@ -15150,91 +15139,19 @@ ${r.consoleErrors.join("\n")}` : ""}`
15150
15139
  cellWidth: external_exports.number().optional().default(400).describe("Width of each cell (px)"),
15151
15140
  cellHeight: external_exports.number().optional().default(300).describe("Height of each cell (px)")
15152
15141
  },
15153
- async ({ cells, framework, columns, cellWidth, cellHeight }) => {
15154
- try {
15142
+ wrapHandler(
15143
+ async ({ cells, framework, columns, cellWidth, cellHeight }) => {
15155
15144
  const result = await useCase.renderGrid(cells, framework, {
15156
15145
  columns,
15157
15146
  viewport: { width: cellWidth, height: cellHeight }
15158
15147
  });
15159
- const content = [
15160
- {
15161
- type: "image",
15162
- data: result.image,
15163
- mimeType: "image/png"
15164
- },
15165
- {
15166
- type: "text",
15167
- text: `Grid: ${result.cells} cells, ${columns} columns \u2014 ${result.width}x${result.height}`
15168
- }
15169
- ];
15170
- return { content };
15171
- } catch (error51) {
15172
- const msg = error51 instanceof Error ? error51.message : String(error51);
15173
- return {
15174
- content: [
15175
- { type: "text", text: `Grid render failed: ${msg}` }
15176
- ],
15177
- isError: true
15178
- };
15179
- }
15180
- }
15181
- );
15182
- server2.tool(
15183
- "render_theme",
15184
- "Render a component in both light and dark mode side-by-side. Returns 2 labeled screenshots for quick theme verification.",
15185
- {
15186
- code: external_exports.string().describe("Component code to render"),
15187
- framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework"),
15188
- width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
15189
- height: external_exports.number().optional().default(800).describe("Viewport height (px)")
15190
- },
15191
- async ({ code, framework, width, height }) => {
15192
- try {
15193
- const start = performance.now();
15194
- const [lightResults, darkResults] = await Promise.all([
15195
- useCase.render(code, framework, {
15196
- viewport: { width, height },
15197
- fullPage: true,
15198
- engines: ["chromium"],
15199
- darkMode: false
15200
- }),
15201
- useCase.render(code, framework, {
15202
- viewport: { width, height },
15203
- fullPage: true,
15204
- engines: ["chromium"],
15205
- darkMode: true
15206
- })
15207
- ]);
15208
- const elapsed = Math.round(performance.now() - start);
15209
- const content = [
15210
- { type: "text", text: "\u2600\uFE0F Light mode:" },
15211
- {
15212
- type: "image",
15213
- data: lightResults[0].image,
15214
- mimeType: "image/png"
15215
- },
15216
- { type: "text", text: "\u{1F319} Dark mode:" },
15217
- {
15218
- type: "image",
15219
- data: darkResults[0].image,
15220
- mimeType: "image/png"
15221
- },
15222
- {
15223
- type: "text",
15224
- text: `${lightResults[0].width}x${lightResults[0].height} (${elapsed}ms)`
15225
- }
15226
- ];
15148
+ const content = imageTextContent(
15149
+ result.image,
15150
+ `Grid: ${result.cells} cells, ${columns} columns \u2014 ${result.width}x${result.height}`
15151
+ );
15227
15152
  return { content };
15228
- } catch (error51) {
15229
- const msg = error51 instanceof Error ? error51.message : String(error51);
15230
- return {
15231
- content: [
15232
- { type: "text", text: `Theme render failed: ${msg}` }
15233
- ],
15234
- isError: true
15235
- };
15236
15153
  }
15237
- }
15154
+ )
15238
15155
  );
15239
15156
  server2.tool(
15240
15157
  "render_matrix",
@@ -15257,8 +15174,8 @@ ${r.consoleErrors.join("\n")}` : ""}`
15257
15174
  css: external_exports.string().optional().describe("Custom CSS to inject (design tokens, variables, etc)"),
15258
15175
  tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
15259
15176
  },
15260
- async ({ code, framework, viewports, themes, css, tailwindVersion }) => {
15261
- try {
15177
+ wrapHandler(
15178
+ async ({ code, framework, viewports, themes, css, tailwindVersion }) => {
15262
15179
  const start = performance.now();
15263
15180
  const resolvedViewports = viewports.map((v) => {
15264
15181
  if (typeof v === "string") {
@@ -15282,81 +15199,31 @@ ${r.consoleErrors.join("\n")}` : ""}`
15282
15199
  }
15283
15200
  );
15284
15201
  const elapsed = Math.round(performance.now() - start);
15285
- const content = results.flatMap((r) => [
15286
- {
15287
- type: "image",
15288
- data: r.image,
15289
- mimeType: "image/png"
15290
- },
15291
- {
15292
- type: "text",
15293
- text: `[${r.viewport}/${r.theme}] ${r.width}x${r.height}${r.consoleErrors.length ? `
15202
+ const content = results.flatMap(
15203
+ (r) => imageTextContent(
15204
+ r.image,
15205
+ `[${r.viewport}/${r.theme}] ${r.width}x${r.height}${r.consoleErrors.length ? `
15294
15206
  \u26A0\uFE0F Console errors:
15295
15207
  ${r.consoleErrors.join("\n")}` : ""}`
15296
- }
15297
- ]);
15208
+ )
15209
+ );
15298
15210
  content.push({
15299
15211
  type: "text",
15300
15212
  text: `Matrix: ${resolvedViewports.length} viewports x ${themes.length} themes = ${results.length} renders (${elapsed}ms)`
15301
15213
  });
15302
15214
  return { content };
15303
- } catch (error51) {
15304
- const msg = error51 instanceof Error ? error51.message : String(error51);
15305
- return {
15306
- content: [
15307
- { type: "text", text: `Matrix render failed: ${msg}` }
15308
- ],
15309
- isError: true
15310
- };
15311
15215
  }
15312
- }
15313
- );
15314
- server2.tool(
15315
- "capture_animation",
15316
- "Capture multiple frames of a CSS animation or transition over time. Returns sequential screenshots to verify animation behavior, timing, and smoothness.",
15317
- {
15318
- code: external_exports.string().describe("Component code with CSS animations/transitions"),
15319
- framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
15320
- frames: external_exports.number().optional().default(5).describe("Number of frames to capture"),
15321
- duration: external_exports.number().optional().default(1e3).describe("Total capture duration in ms"),
15322
- width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
15323
- height: external_exports.number().optional().default(800).describe("Viewport height (px)")
15324
- },
15325
- async ({ code, framework, frames, duration: duration3, width, height }) => {
15326
- try {
15327
- const results = await useCase.captureAnimation(code, framework, {
15328
- frames,
15329
- duration: duration3,
15330
- viewport: { width, height }
15331
- });
15332
- const content = results.flatMap((r) => [
15333
- {
15334
- type: "image",
15335
- data: r.image,
15336
- mimeType: "image/png"
15337
- },
15338
- {
15339
- type: "text",
15340
- text: `[${r.timestamp}ms]`
15341
- }
15342
- ]);
15343
- return { content };
15344
- } catch (error51) {
15345
- const msg = error51 instanceof Error ? error51.message : String(error51);
15346
- return {
15347
- content: [
15348
- {
15349
- type: "text",
15350
- text: `Animation capture failed: ${msg}`
15351
- }
15352
- ],
15353
- isError: true
15354
- };
15355
- }
15356
- }
15216
+ )
15357
15217
  );
15358
15218
  }
15359
15219
 
15220
+ // src/tools/render-tools.ts
15221
+ function registerRenderTools(server2, useCase) {
15222
+ registerRenderFileTools(server2, useCase);
15223
+ registerRenderVisualTools(server2, useCase);
15224
+ registerRenderAnimationTools(server2, useCase);
15225
+ }
15226
+
15360
15227
  // src/tools/screenshot-tools.ts
15361
15228
  function registerScreenshotTools(server2, useCase) {
15362
15229
  server2.tool(
@@ -15376,17 +15243,17 @@ function registerScreenshotTools(server2, useCase) {
15376
15243
  "Number of retries with exponential backoff (300ms, 600ms, 1200ms...) if the page fails to load"
15377
15244
  )
15378
15245
  },
15379
- async ({
15380
- url: url2,
15381
- engines,
15382
- width,
15383
- height,
15384
- fullPage,
15385
- waitForSelector,
15386
- waitForNetworkIdle,
15387
- retryOnError
15388
- }) => {
15389
- try {
15246
+ wrapHandler(
15247
+ async ({
15248
+ url: url2,
15249
+ engines,
15250
+ width,
15251
+ height,
15252
+ fullPage,
15253
+ waitForSelector,
15254
+ waitForNetworkIdle,
15255
+ retryOnError
15256
+ }) => {
15390
15257
  const results = await useCase.screenshotUrlWithRetry(url2, {
15391
15258
  viewport: { width, height },
15392
15259
  engines,
@@ -15409,20 +15276,8 @@ ${r.consoleErrors.join("\n")}` : ""}`
15409
15276
  }
15410
15277
  ]);
15411
15278
  return { content };
15412
- } catch (error51) {
15413
- const msg = error51 instanceof Error ? error51.message : String(error51);
15414
- const retryInfo = retryOnError > 0 ? ` (after ${retryOnError + 1} attempts)` : "";
15415
- return {
15416
- content: [
15417
- {
15418
- type: "text",
15419
- text: `Screenshot failed${retryInfo}: ${msg}`
15420
- }
15421
- ],
15422
- isError: true
15423
- };
15424
15279
  }
15425
- }
15280
+ )
15426
15281
  );
15427
15282
  }
15428
15283
 
@@ -15442,16 +15297,16 @@ function registerSnapshotTools(server2, useCase) {
15442
15297
  darkMode: external_exports.boolean().optional().default(false).describe("Render with dark mode"),
15443
15298
  tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
15444
15299
  },
15445
- async ({
15446
- name,
15447
- code,
15448
- framework,
15449
- width,
15450
- height,
15451
- darkMode,
15452
- tailwindVersion
15453
- }) => {
15454
- try {
15300
+ wrapHandler(
15301
+ async ({
15302
+ name,
15303
+ code,
15304
+ framework,
15305
+ width,
15306
+ height,
15307
+ darkMode,
15308
+ tailwindVersion
15309
+ }) => {
15455
15310
  const result = await useCase.save(name, code, framework, {
15456
15311
  viewport: { width, height },
15457
15312
  darkMode,
@@ -15470,16 +15325,8 @@ function registerSnapshotTools(server2, useCase) {
15470
15325
  }
15471
15326
  ]
15472
15327
  };
15473
- } catch (error51) {
15474
- const msg = error51 instanceof Error ? error51.message : String(error51);
15475
- return {
15476
- content: [
15477
- { type: "text", text: `Snapshot save failed: ${msg}` }
15478
- ],
15479
- isError: true
15480
- };
15481
15328
  }
15482
- }
15329
+ )
15483
15330
  );
15484
15331
  server2.tool(
15485
15332
  "snapshot_check",
@@ -15493,16 +15340,16 @@ function registerSnapshotTools(server2, useCase) {
15493
15340
  darkMode: external_exports.boolean().optional().default(false).describe("Render with dark mode"),
15494
15341
  tailwindVersion: external_exports.enum(["3", "4"]).optional().default("3").describe("Tailwind CSS version (3 or 4)")
15495
15342
  },
15496
- async ({
15497
- name,
15498
- code,
15499
- framework,
15500
- width,
15501
- height,
15502
- darkMode,
15503
- tailwindVersion
15504
- }) => {
15505
- try {
15343
+ wrapHandler(
15344
+ async ({
15345
+ name,
15346
+ code,
15347
+ framework,
15348
+ width,
15349
+ height,
15350
+ darkMode,
15351
+ tailwindVersion
15352
+ }) => {
15506
15353
  const result = await useCase.check(name, code, framework, {
15507
15354
  viewport: { width, height },
15508
15355
  darkMode,
@@ -15538,16 +15385,8 @@ function registerSnapshotTools(server2, useCase) {
15538
15385
  }
15539
15386
  ];
15540
15387
  return { content };
15541
- } catch (error51) {
15542
- const msg = error51 instanceof Error ? error51.message : String(error51);
15543
- return {
15544
- content: [
15545
- { type: "text", text: `Snapshot check failed: ${msg}` }
15546
- ],
15547
- isError: true
15548
- };
15549
15388
  }
15550
- }
15389
+ )
15551
15390
  );
15552
15391
  server2.tool(
15553
15392
  "snapshot_list",
@@ -15581,6 +15420,133 @@ ${lines.join("\n")}`
15581
15420
  );
15582
15421
  }
15583
15422
 
15423
+ // src/tools/watch-tools.ts
15424
+ function registerWatchTools(server2, useCase) {
15425
+ server2.tool(
15426
+ "watch_start",
15427
+ "Start watching component files for changes. On every save, the component is automatically rendered and you will receive a notification \u2014 call watch_get_latest(id) to retrieve the rendered screenshot. Use this to create a live feedback loop while editing UI \u2014 the AI sees each change without you needing to call render_file manually.",
15428
+ {
15429
+ patterns: external_exports.array(external_exports.string()).describe(
15430
+ "Glob patterns or absolute file paths to watch (e.g. ['src/components/Button.tsx'])"
15431
+ ),
15432
+ props: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("Props to pass to the component on each render")
15433
+ },
15434
+ async ({ patterns, props }) => {
15435
+ const session = useCase.start(patterns, props, async (event) => {
15436
+ if (event.error) {
15437
+ await server2.server.notification({
15438
+ method: "notifications/message",
15439
+ params: {
15440
+ level: "error",
15441
+ logger: "frameshot/watch",
15442
+ data: `[${event.sessionId}] \u274C ${event.filePath}: ${event.error}`
15443
+ }
15444
+ });
15445
+ return;
15446
+ }
15447
+ const summary = `[${event.sessionId}] \u2713 ${event.filePath} \u2014 ${event.width}\xD7${event.height} \xB7 ${event.elapsedMs}ms \xB7 ${event.mode}` + (event.consoleErrors.length ? `
15448
+ \u26A0\uFE0F ${event.consoleErrors.join("\n")}` : "") + `
15449
+ Call watch_get_latest("${event.sessionId}") to see the render.`;
15450
+ await server2.server.notification({
15451
+ method: "notifications/message",
15452
+ params: {
15453
+ level: "info",
15454
+ logger: "frameshot/watch",
15455
+ data: summary
15456
+ }
15457
+ });
15458
+ });
15459
+ return {
15460
+ content: [
15461
+ {
15462
+ type: "text",
15463
+ text: `\u2713 Watch started (${session.id})
15464
+ Watching: ${patterns.join(", ")}
15465
+
15466
+ Every time a matched file is saved, it will be rendered automatically and a notification will appear in your MCP log stream. Call \`watch_get_latest\` with id "${session.id}" to retrieve the screenshot.
15467
+
15468
+ Call \`watch_stop\` with id "${session.id}" to stop.`
15469
+ }
15470
+ ]
15471
+ };
15472
+ }
15473
+ );
15474
+ server2.tool(
15475
+ "watch_stop",
15476
+ "Stop a running file watcher started with watch_start.",
15477
+ {
15478
+ id: external_exports.string().describe("Watch session ID returned by watch_start (e.g. 'watch-1')")
15479
+ },
15480
+ async ({ id }) => {
15481
+ const stopped = useCase.stop(id);
15482
+ return {
15483
+ content: [
15484
+ {
15485
+ type: "text",
15486
+ text: stopped ? `\u2713 Watch session "${id}" stopped.` : `No active watch session with id "${id}".`
15487
+ }
15488
+ ]
15489
+ };
15490
+ }
15491
+ );
15492
+ server2.tool(
15493
+ "watch_list",
15494
+ "List all currently active file watch sessions.",
15495
+ {},
15496
+ async () => {
15497
+ const sessions = useCase.activeSessions();
15498
+ if (sessions.length === 0) {
15499
+ return {
15500
+ content: [{ type: "text", text: "No active watch sessions." }]
15501
+ };
15502
+ }
15503
+ const lines = sessions.map(
15504
+ (s) => `${s.id}: watching ${s.patterns.join(", ")}`
15505
+ );
15506
+ return {
15507
+ content: [{ type: "text", text: lines.join("\n") }]
15508
+ };
15509
+ }
15510
+ );
15511
+ server2.tool(
15512
+ "watch_get_latest",
15513
+ "Get the latest rendered screenshot from a watch session. Call this after receiving a watch notification to see the current state of the component.",
15514
+ {
15515
+ id: external_exports.string().describe("Watch session ID from watch_start")
15516
+ },
15517
+ async ({ id }) => {
15518
+ const render = useCase.getLatestRender(id);
15519
+ if (!render) {
15520
+ return {
15521
+ content: [
15522
+ { type: "text", text: `No render yet for session "${id}".` }
15523
+ ]
15524
+ };
15525
+ }
15526
+ if (render.error) {
15527
+ return {
15528
+ content: [
15529
+ { type: "text", text: `Last render failed: ${render.error}` }
15530
+ ]
15531
+ };
15532
+ }
15533
+ return {
15534
+ content: [
15535
+ {
15536
+ type: "image",
15537
+ data: render.image,
15538
+ mimeType: "image/png"
15539
+ },
15540
+ {
15541
+ type: "text",
15542
+ text: `${render.filePath} \u2014 ${render.width}\xD7${render.height} \xB7 ${render.elapsedMs}ms \xB7 ${render.mode}`
15543
+ }
15544
+ ]
15545
+ };
15546
+ }
15547
+ );
15548
+ }
15549
+
15584
15550
  // src/tools/register.ts
15585
15551
  function registerAllTools(server2, useCases) {
15586
15552
  registerRenderTools(server2, useCases.render);
@@ -15589,36 +15555,18 @@ function registerAllTools(server2, useCases) {
15589
15555
  registerAuditTools(server2, useCases.audit);
15590
15556
  registerSnapshotTools(server2, useCases.snapshot);
15591
15557
  registerCatalogTools(server2, useCases.catalog);
15558
+ registerWatchTools(server2, useCases.watch);
15592
15559
  }
15593
15560
 
15594
15561
  // src/index.ts
15595
- var browserPool = new BrowserPool();
15596
- var htmlBuilder = new HtmlBuilder();
15597
- var imageComparator = new ImageComparator();
15598
- var snapshotStore = new SnapshotStore();
15599
- var viteBundler = new ViteBundler();
15600
- var renderUseCase = new RenderUseCase(
15601
- browserPool,
15602
- htmlBuilder,
15603
- imageComparator,
15604
- viteBundler
15605
- );
15606
- var screenshotUseCase = new ScreenshotUseCase(browserPool);
15607
- var diffUseCase = new DiffUseCase(renderUseCase, imageComparator);
15608
- var auditUseCase = new AuditUseCase(browserPool, htmlBuilder);
15609
- var snapshotUseCase = new SnapshotUseCase(
15610
- snapshotStore,
15611
- renderUseCase,
15612
- diffUseCase
15613
- );
15614
- var catalogUseCase = new CatalogUseCase(renderUseCase);
15562
+ var container = createContainer();
15615
15563
  async function ensureBrowser() {
15616
15564
  try {
15617
- await browserPool.warmup(["chromium"]);
15565
+ await container.pool.warmup(["chromium"]);
15618
15566
  } catch {
15619
15567
  try {
15620
15568
  execSync("npx playwright install chromium", { stdio: "pipe" });
15621
- await browserPool.warmup(["chromium"]);
15569
+ await container.pool.warmup(["chromium"]);
15622
15570
  } catch {
15623
15571
  }
15624
15572
  }
@@ -15626,25 +15574,24 @@ async function ensureBrowser() {
15626
15574
  await ensureBrowser();
15627
15575
  var server = new McpServer({
15628
15576
  name: "frameshot",
15629
- version: "0.5.0"
15577
+ version: "0.8.0"
15630
15578
  });
15631
15579
  registerAllTools(server, {
15632
- render: renderUseCase,
15633
- screenshot: screenshotUseCase,
15634
- diff: diffUseCase,
15635
- audit: auditUseCase,
15636
- snapshot: snapshotUseCase,
15637
- catalog: catalogUseCase
15580
+ render: container.renderUseCase,
15581
+ screenshot: container.screenshotUseCase,
15582
+ diff: container.diffUseCase,
15583
+ audit: container.auditUseCase,
15584
+ snapshot: container.snapshotUseCase,
15585
+ catalog: container.catalogUseCase,
15586
+ watch: container.watchUseCase
15638
15587
  });
15639
15588
  var transport = new StdioServerTransport();
15640
15589
  await server.connect(transport);
15641
15590
  process.on("SIGINT", async () => {
15642
- await viteBundler.shutdown();
15643
- await browserPool.shutdown();
15591
+ await container.shutdown();
15644
15592
  process.exit(0);
15645
15593
  });
15646
15594
  process.on("SIGTERM", async () => {
15647
- await viteBundler.shutdown();
15648
- await browserPool.shutdown();
15595
+ await container.shutdown();
15649
15596
  process.exit(0);
15650
15597
  });