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.
@@ -0,0 +1,241 @@
1
+ import { Page } from 'playwright';
2
+
3
+ type Engine = "chromium" | "firefox" | "webkit";
4
+ type Framework = "html" | "react" | "vue" | "svelte";
5
+ type Theme = "light" | "dark";
6
+ interface Viewport {
7
+ width: number;
8
+ height: number;
9
+ }
10
+ interface RenderOptions {
11
+ viewport: Viewport;
12
+ deviceScaleFactor?: number;
13
+ engines: Engine[];
14
+ fullPage: boolean;
15
+ darkMode: boolean;
16
+ css: string;
17
+ tailwindVersion: "3" | "4";
18
+ waitFor: number;
19
+ }
20
+ interface ScreenshotResult {
21
+ engine: Engine;
22
+ image: string;
23
+ width: number;
24
+ height: number;
25
+ consoleErrors: string[];
26
+ }
27
+ interface DiffResult {
28
+ before: string;
29
+ after: string;
30
+ diff: string;
31
+ diffPixels: number;
32
+ totalPixels: number;
33
+ diffPercentage: number;
34
+ }
35
+ interface ReferenceDiffResult {
36
+ rendered: string;
37
+ diff: string;
38
+ diffPixels: number;
39
+ totalPixels: number;
40
+ diffPercentage: number;
41
+ passed: boolean;
42
+ }
43
+ interface A11yViolation {
44
+ id: string;
45
+ impact: string;
46
+ description: string;
47
+ helpUrl: string;
48
+ nodes: {
49
+ html: string;
50
+ target: string[];
51
+ }[];
52
+ }
53
+ interface A11yResult {
54
+ violations: A11yViolation[];
55
+ passes: number;
56
+ incomplete: number;
57
+ }
58
+ interface PerfMetrics {
59
+ renderTimeMs: number;
60
+ domElements: number;
61
+ domDepth: number;
62
+ scriptCount: number;
63
+ styleSheetCount: number;
64
+ imageCount: number;
65
+ totalDomSize: number;
66
+ }
67
+ interface AnimationFrame {
68
+ timestamp: number;
69
+ image: string;
70
+ }
71
+ interface Interaction {
72
+ action: "click" | "hover" | "focus" | "type" | "wait";
73
+ selector?: string;
74
+ value?: string;
75
+ ms?: number;
76
+ }
77
+ interface GridCell {
78
+ label: string;
79
+ code: string;
80
+ }
81
+ interface Snapshot {
82
+ image: string;
83
+ timestamp: number;
84
+ }
85
+ interface CatalogEntry {
86
+ path: string;
87
+ framework: Framework;
88
+ image: string;
89
+ width: number;
90
+ height: number;
91
+ consoleErrors: string[];
92
+ }
93
+ declare const DEVICE_PRESETS: Record<string, Viewport>;
94
+ declare const EXT_TO_FRAMEWORK: Record<string, Framework>;
95
+
96
+ declare class BrowserPool {
97
+ private pool;
98
+ warmup(engines: Engine[]): Promise<void>;
99
+ getPage(engine: Engine): Promise<Page>;
100
+ setViewport(engine: Engine, viewport: Viewport): Promise<void>;
101
+ shutdown(): Promise<void>;
102
+ private getSlot;
103
+ }
104
+
105
+ declare class HtmlBuilder {
106
+ build(code: string, framework: Framework, options: {
107
+ darkMode: boolean;
108
+ css: string;
109
+ tailwindVersion: "3" | "4";
110
+ }): string;
111
+ }
112
+
113
+ declare class ImageComparator {
114
+ diff(imageA: string, imageB: string, threshold?: number): {
115
+ diff: string;
116
+ diffPixels: number;
117
+ totalPixels: number;
118
+ diffPercentage: number;
119
+ };
120
+ composite(images: Buffer[], columns: number, labelHeight?: number): {
121
+ image: string;
122
+ width: number;
123
+ height: number;
124
+ };
125
+ }
126
+
127
+ declare class SnapshotStore {
128
+ private store;
129
+ save(key: string, image: string): void;
130
+ get(key: string): Snapshot | undefined;
131
+ list(): {
132
+ key: string;
133
+ timestamp: number;
134
+ }[];
135
+ }
136
+
137
+ declare class AuditUseCase {
138
+ private readonly pool;
139
+ private readonly htmlBuilder;
140
+ constructor(pool: BrowserPool, htmlBuilder: HtmlBuilder);
141
+ auditA11y(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<A11yResult>;
142
+ perfAudit(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<PerfMetrics>;
143
+ }
144
+
145
+ declare class RenderUseCase {
146
+ private readonly pool;
147
+ private readonly htmlBuilder;
148
+ private readonly imageComparator;
149
+ constructor(pool: BrowserPool, htmlBuilder: HtmlBuilder, imageComparator: ImageComparator);
150
+ render(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<ScreenshotResult[]>;
151
+ renderInteraction(code: string, framework: Framework, interactions: Interaction[], options?: Partial<RenderOptions>): Promise<{
152
+ image: string;
153
+ width: number;
154
+ height: number;
155
+ }>;
156
+ captureAnimation(code: string, framework: Framework, options?: Partial<RenderOptions> & {
157
+ frames?: number;
158
+ duration?: number;
159
+ }): Promise<AnimationFrame[]>;
160
+ renderGrid(cells: GridCell[], framework: Framework, options?: Partial<RenderOptions> & {
161
+ columns?: number;
162
+ }): Promise<{
163
+ image: string;
164
+ width: number;
165
+ height: number;
166
+ cells: number;
167
+ }>;
168
+ renderMatrix(code: string, framework: Framework, viewports: {
169
+ label: string;
170
+ width: number;
171
+ height: number;
172
+ }[], themes: Theme[], options?: Partial<Omit<RenderOptions, "viewport" | "darkMode">>): Promise<{
173
+ viewport: string;
174
+ theme: Theme;
175
+ image: string;
176
+ width: number;
177
+ height: number;
178
+ consoleErrors: string[];
179
+ }[]>;
180
+ private renderHtml;
181
+ private resolveOptions;
182
+ }
183
+
184
+ declare class CatalogUseCase {
185
+ private readonly renderUseCase;
186
+ constructor(renderUseCase: RenderUseCase);
187
+ renderCatalog(directory: string, options?: Partial<RenderOptions> & {
188
+ recursive?: boolean;
189
+ }): Promise<CatalogEntry[]>;
190
+ private scanDirectory;
191
+ private detectFramework;
192
+ }
193
+
194
+ declare class DiffUseCase {
195
+ private readonly renderUseCase;
196
+ private readonly imageComparator;
197
+ constructor(renderUseCase: RenderUseCase, imageComparator: ImageComparator);
198
+ diffComponent(before: string, after: string, framework: Framework, options?: Partial<RenderOptions>): Promise<DiffResult>;
199
+ diffFromReference(code: string, framework: Framework, referenceImage: string, options?: Partial<RenderOptions> & {
200
+ threshold?: number;
201
+ }): Promise<ReferenceDiffResult>;
202
+ }
203
+
204
+ interface ScreenshotUrlOptions {
205
+ viewport?: {
206
+ width: number;
207
+ height: number;
208
+ };
209
+ engines?: Engine[];
210
+ fullPage?: boolean;
211
+ waitFor?: number;
212
+ waitForSelector?: string;
213
+ waitForNetworkIdle?: boolean;
214
+ }
215
+ declare class ScreenshotUseCase {
216
+ private readonly pool;
217
+ constructor(pool: BrowserPool);
218
+ screenshotUrl(url: string, options?: ScreenshotUrlOptions): Promise<ScreenshotResult[]>;
219
+ screenshotUrlWithRetry(url: string, options?: ScreenshotUrlOptions & {
220
+ retryCount?: number;
221
+ }): Promise<ScreenshotResult[]>;
222
+ }
223
+
224
+ declare class SnapshotUseCase {
225
+ private readonly store;
226
+ private readonly renderUseCase;
227
+ private readonly diffUseCase;
228
+ constructor(store: SnapshotStore, renderUseCase: RenderUseCase, diffUseCase: DiffUseCase);
229
+ save(key: string, code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<{
230
+ image: string;
231
+ width: number;
232
+ height: number;
233
+ }>;
234
+ check(key: string, code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<ReferenceDiffResult | null>;
235
+ list(): {
236
+ key: string;
237
+ timestamp: number;
238
+ }[];
239
+ }
240
+
241
+ export { type A11yResult, type A11yViolation, type AnimationFrame, AuditUseCase, BrowserPool, type CatalogEntry, CatalogUseCase, DEVICE_PRESETS, type DiffResult, DiffUseCase, EXT_TO_FRAMEWORK, type Engine, type Framework, type GridCell, HtmlBuilder, ImageComparator, type Interaction, type PerfMetrics, type ReferenceDiffResult, type RenderOptions, RenderUseCase, type ScreenshotResult, ScreenshotUseCase, type Snapshot, SnapshotStore, SnapshotUseCase, type Theme, type Viewport };
@@ -0,0 +1,28 @@
1
+ import {
2
+ AuditUseCase,
3
+ BrowserPool,
4
+ CatalogUseCase,
5
+ DEVICE_PRESETS,
6
+ DiffUseCase,
7
+ EXT_TO_FRAMEWORK,
8
+ HtmlBuilder,
9
+ ImageComparator,
10
+ RenderUseCase,
11
+ ScreenshotUseCase,
12
+ SnapshotStore,
13
+ SnapshotUseCase
14
+ } from "./chunk-47YJG5HR.js";
15
+ export {
16
+ AuditUseCase,
17
+ BrowserPool,
18
+ CatalogUseCase,
19
+ DEVICE_PRESETS,
20
+ DiffUseCase,
21
+ EXT_TO_FRAMEWORK,
22
+ HtmlBuilder,
23
+ ImageComparator,
24
+ RenderUseCase,
25
+ ScreenshotUseCase,
26
+ SnapshotStore,
27
+ SnapshotUseCase
28
+ };
package/package.json CHANGED
@@ -1,22 +1,38 @@
1
1
  {
2
2
  "name": "frameshot-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.6.0",
4
4
  "description": "Instant cross-browser component preview for AI agents. One MCP call, one screenshot — no dev server, no Storybook, no ceremony.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ },
12
+ "./renderer": {
13
+ "import": "./dist/renderer.js",
14
+ "types": "./dist/renderer.d.ts"
15
+ }
16
+ },
7
17
  "bin": {
8
18
  "frameshot-mcp": "dist/index.js"
9
19
  },
10
20
  "files": [
11
21
  "dist",
22
+ "scripts",
23
+ "action.yml",
12
24
  "README.md",
13
25
  "LICENSE"
14
26
  ],
15
27
  "scripts": {
16
- "build": "tsup src/index.ts --format esm --dts",
17
- "dev": "tsup src/index.ts --format esm --watch",
28
+ "build": "tsup src/index.ts src/renderer.ts --format esm --dts",
29
+ "dev": "tsup src/index.ts src/renderer.ts --format esm --watch",
18
30
  "start": "node dist/index.js",
19
31
  "typecheck": "tsc --noEmit",
32
+ "lint": "biome check src/",
33
+ "lint:fix": "biome check --write src/",
34
+ "format": "biome format --write src/",
35
+ "test": "vitest run",
20
36
  "prepublishOnly": "npm run build"
21
37
  },
22
38
  "keywords": [
@@ -49,13 +65,20 @@
49
65
  },
50
66
  "dependencies": {
51
67
  "@modelcontextprotocol/sdk": "^1.12.1",
52
- "playwright": "^1.52.0"
68
+ "axe-core": "^4.12.1",
69
+ "pixelmatch": "^7.2.0",
70
+ "playwright": "^1.52.0",
71
+ "pngjs": "^7.0.0"
53
72
  },
54
73
  "devDependencies": {
74
+ "@biomejs/biome": "^2.5.0",
55
75
  "@types/node": "^22.0.0",
76
+ "@types/pngjs": "^6.0.5",
56
77
  "tsup": "^8.0.0",
57
- "typescript": "^5.7.0"
78
+ "typescript": "^5.7.0",
79
+ "vitest": "^4.1.9"
58
80
  },
81
+ "mcpName": "io.github.kamegoro/frameshot-mcp",
59
82
  "engines": {
60
83
  "node": ">=20.0.0"
61
84
  }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { basename, extname, join } from "node:path";
6
+
7
+ const {
8
+ INPUT_PATHS = "",
9
+ INPUT_FRAMEWORK = "react",
10
+ INPUT_WIDTH = "1280",
11
+ INPUT_HEIGHT = "800",
12
+ INPUT_DARK_MODE = "false",
13
+ INPUT_TAILWIND_VERSION = "3",
14
+ GITHUB_WORKSPACE = process.cwd(),
15
+ } = process.env;
16
+
17
+ const outputDir = join(GITHUB_WORKSPACE, ".frameshot-screenshots");
18
+ mkdirSync(outputDir, { recursive: true });
19
+
20
+ const patterns = INPUT_PATHS.split(/[,\n]/)
21
+ .map((p) => p.trim())
22
+ .filter(Boolean);
23
+
24
+ const EXT_TO_FRAMEWORK = {
25
+ ".jsx": "react",
26
+ ".tsx": "react",
27
+ ".vue": "vue",
28
+ ".svelte": "svelte",
29
+ ".html": "html",
30
+ ".htm": "html",
31
+ };
32
+
33
+ async function main() {
34
+ const { render, warmup, shutdown } = await import("frameshot-mcp/renderer");
35
+
36
+ await warmup(["chromium"]);
37
+
38
+ const allFiles = [];
39
+ for (const pattern of patterns) {
40
+ try {
41
+ const result = execSync(
42
+ `find ${GITHUB_WORKSPACE} -path '${pattern}' -type f 2>/dev/null`,
43
+ { encoding: "utf-8" },
44
+ );
45
+ allFiles.push(...result.split("\n").filter(Boolean));
46
+ } catch {
47
+ // Pattern didn't match
48
+ }
49
+ }
50
+
51
+ if (allFiles.length === 0) {
52
+ console.log("No component files found matching the specified patterns.");
53
+ await shutdown();
54
+ process.exit(0);
55
+ }
56
+
57
+ console.log(`Found ${allFiles.length} component file(s) to render...`);
58
+
59
+ for (const filePath of allFiles) {
60
+ const ext = extname(filePath).toLowerCase();
61
+ const framework = EXT_TO_FRAMEWORK[ext] || INPUT_FRAMEWORK;
62
+ const name = basename(filePath, extname(filePath));
63
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
64
+
65
+ try {
66
+ console.log(` Rendering: ${filePath}`);
67
+ const code = readFileSync(filePath, "utf-8");
68
+ const [result] = await render(code, framework, {
69
+ width: parseInt(INPUT_WIDTH),
70
+ height: parseInt(INPUT_HEIGHT),
71
+ darkMode: INPUT_DARK_MODE === "true",
72
+ tailwindVersion: INPUT_TAILWIND_VERSION,
73
+ engines: ["chromium"],
74
+ });
75
+ const outPath = join(outputDir, `${safeName}.png`);
76
+ writeFileSync(outPath, Buffer.from(result.image, "base64"));
77
+ console.log(` ✓ ${safeName}.png (${result.width}x${result.height})`);
78
+ } catch (error) {
79
+ console.error(` ✗ Failed: ${filePath} — ${error.message}`);
80
+ }
81
+ }
82
+
83
+ await shutdown();
84
+ console.log(`\nDone. Screenshots saved to ${outputDir}`);
85
+ }
86
+
87
+ main().catch((e) => {
88
+ console.error(e);
89
+ process.exit(1);
90
+ });
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # Setup GitHub labels for the frameshot repository.
3
+ # Usage: ./scripts/setup-labels.sh [owner/repo]
4
+ # Requires: gh CLI authenticated
5
+
6
+ set -euo pipefail
7
+
8
+ REPO="${1:-kamegoro/frameshot}"
9
+
10
+ # Delete default labels that add noise
11
+ for label in "invalid" "wontfix" "help wanted" "question"; do
12
+ gh label delete "$label" --repo "$REPO" --yes 2>/dev/null || true
13
+ done
14
+
15
+ # --- Status labels (S-) ---
16
+ gh label create "S-needs triage" --color "D93F0B" --description "Awaiting maintainer review" --repo "$REPO" --force
17
+ gh label create "S-accepted" --color "0E8A16" --description "Confirmed and ready for work" --repo "$REPO" --force
18
+ gh label create "S-in progress" --color "1D76DB" --description "Currently being worked on" --repo "$REPO" --force
19
+ gh label create "S-blocked" --color "B60205" --description "Waiting on external dependency" --repo "$REPO" --force
20
+
21
+ # --- Priority labels (P-) ---
22
+ gh label create "P0-critical" --color "B60205" --description "Production broken, fix immediately" --repo "$REPO" --force
23
+ gh label create "P1-high" --color "D93F0B" --description "Important, fix this week" --repo "$REPO" --force
24
+ gh label create "P2-medium" --color "FBCA04" --description "Normal priority" --repo "$REPO" --force
25
+ gh label create "P3-low" --color "0075CA" --description "Nice to have, no urgency" --repo "$REPO" --force
26
+
27
+ # --- Type labels ---
28
+ gh label create "bug" --color "D73A4A" --description "Something isn't working" --repo "$REPO" --force
29
+ gh label create "enhancement" --color "A2EEEF" --description "New feature or improvement" --repo "$REPO" --force
30
+ gh label create "documentation" --color "0075CA" --description "Documentation improvements" --repo "$REPO" --force
31
+ gh label create "performance" --color "F9D0C4" --description "Performance improvement" --repo "$REPO" --force
32
+ gh label create "breaking change" --color "B60205" --description "Introduces a breaking change" --repo "$REPO" --force
33
+
34
+ # --- Area labels (A-) ---
35
+ gh label create "A-rendering" --color "C5DEF5" --description "Component rendering pipeline" --repo "$REPO" --force
36
+ gh label create "A-screenshot" --color "C5DEF5" --description "URL screenshot capture" --repo "$REPO" --force
37
+ gh label create "A-diff" --color "C5DEF5" --description "Visual regression diffing" --repo "$REPO" --force
38
+ gh label create "A-audit" --color "C5DEF5" --description "Accessibility & performance audits" --repo "$REPO" --force
39
+ gh label create "A-mcp-protocol" --color "C5DEF5" --description "MCP transport & tool definitions" --repo "$REPO" --force
40
+ gh label create "A-github-action" --color "C5DEF5" --description "GitHub Action integration" --repo "$REPO" --force
41
+ gh label create "A-config" --color "C5DEF5" --description "Configuration & setup" --repo "$REPO" --force
42
+
43
+ # --- Community labels ---
44
+ gh label create "good first issue" --color "7057FF" --description "Good for newcomers" --repo "$REPO" --force
45
+ gh label create "help wanted" --color "008672" --description "Extra attention is needed" --repo "$REPO" --force
46
+ gh label create "duplicate" --color "CFD3D7" --description "This issue already exists" --repo "$REPO" --force
47
+
48
+ echo "Labels setup complete for $REPO"