frameshot-mcp 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -126
- package/dist/index.js +507 -35
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -2,58 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/frameshot-mcp) [](https://www.npmjs.com/package/frameshot-mcp) [](https://github.com/kamegoro/frameshot) [](https://github.com/kamegoro/frameshot/actions/workflows/ci.yml) [](https://opensource.org/licenses/MIT)
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> Give your AI agent eyes. Render UI components and get screenshots back — in 120ms.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<!-- TODO: demo GIF here -->
|
|
8
|
+
<!--  -->
|
|
8
9
|
|
|
9
|
-
## The Problem
|
|
10
|
-
|
|
11
|
-
You're building UI with an AI agent. The agent writes a component but can't see it. You have to:
|
|
12
|
-
1. Start the dev server
|
|
13
|
-
2. Log in
|
|
14
|
-
3. Navigate to the right page
|
|
15
|
-
4. Scroll to the component
|
|
16
|
-
5. Tell the agent what's wrong
|
|
17
|
-
|
|
18
|
-
**frameshot** skips all of that. The agent renders the component itself, sees the result, and fixes it.
|
|
19
|
-
|
|
20
|
-
## Quick Start
|
|
21
|
-
|
|
22
|
-
### Claude Code
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
claude mcp add frameshot -- npx frameshot-mcp@latest
|
|
26
10
|
```
|
|
27
|
-
|
|
28
|
-
That's it. Now your AI agent can see UI.
|
|
29
|
-
|
|
30
|
-
### Cursor (`.cursor/mcp.json`)
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{
|
|
34
|
-
"mcpServers": {
|
|
35
|
-
"frameshot": {
|
|
36
|
-
"command": "npx",
|
|
37
|
-
"args": ["frameshot-mcp@latest"]
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
11
|
+
AI writes code → AI calls frameshot → AI sees the result → AI self-corrects
|
|
41
12
|
```
|
|
42
13
|
|
|
43
|
-
|
|
14
|
+
## Install
|
|
44
15
|
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
"servers": {
|
|
48
|
-
"frameshot": {
|
|
49
|
-
"command": "npx",
|
|
50
|
-
"args": ["frameshot-mcp@latest"]
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
16
|
+
```bash
|
|
17
|
+
claude mcp add frameshot -- npx frameshot-mcp@latest
|
|
54
18
|
```
|
|
55
19
|
|
|
56
|
-
|
|
20
|
+
<details>
|
|
21
|
+
<summary>Cursor / VS Code / Other</summary>
|
|
57
22
|
|
|
58
23
|
```json
|
|
59
24
|
{
|
|
@@ -66,115 +31,62 @@ That's it. Now your AI agent can see UI.
|
|
|
66
31
|
}
|
|
67
32
|
```
|
|
68
33
|
|
|
69
|
-
>
|
|
34
|
+
</details>
|
|
70
35
|
|
|
71
36
|
## Tools
|
|
72
37
|
|
|
73
|
-
|
|
38
|
+
| Tool | What it does |
|
|
39
|
+
|------|-------------|
|
|
40
|
+
| `render_component` | Render React/Vue/Svelte/HTML → screenshot. Tailwind built-in. |
|
|
41
|
+
| `render_responsive` | Render at mobile + tablet + desktop in one call. |
|
|
42
|
+
| `render_variants` | Render multiple prop/state variants in one call. |
|
|
43
|
+
| `screenshot_url` | Screenshot any URL (e.g. localhost:3000). |
|
|
44
|
+
| `audit_a11y` | Run axe-core accessibility audit on your component. |
|
|
45
|
+
| `diff_component` | Visual regression: compare before/after code, get pixel diff. |
|
|
46
|
+
| `capture_animation` | Capture CSS animation frames over time (multi-screenshot). |
|
|
74
47
|
|
|
75
|
-
|
|
48
|
+
### Example
|
|
76
49
|
|
|
77
50
|
```typescript
|
|
78
51
|
render_component({
|
|
79
|
-
code: `
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<div className="p-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl">
|
|
83
|
-
<h1 className="text-4xl font-bold text-white">Hello World</h1>
|
|
84
|
-
</div>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
`,
|
|
52
|
+
code: `function App() {
|
|
53
|
+
return <div className="p-8 bg-blue-500 text-white rounded-xl">Hello</div>
|
|
54
|
+
}`,
|
|
88
55
|
framework: "react",
|
|
89
|
-
|
|
90
|
-
})
|
|
91
|
-
// Returns 3 screenshots — one per browser engine
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
| Parameter | Default | Description |
|
|
95
|
-
|-----------|---------|-------------|
|
|
96
|
-
| `code` | required | Component source code |
|
|
97
|
-
| `framework` | `"react"` | `"react"` · `"vue"` · `"html"` |
|
|
98
|
-
| `engines` | `["chromium"]` | Browser engines to render in |
|
|
99
|
-
| `width` | `1280` | Viewport width |
|
|
100
|
-
| `height` | `800` | Viewport height |
|
|
101
|
-
| `fullPage` | `true` | Capture full scroll height |
|
|
102
|
-
|
|
103
|
-
### `screenshot_url`
|
|
104
|
-
|
|
105
|
-
Screenshot a running app (e.g. localhost) across browsers.
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
screenshot_url({
|
|
109
|
-
url: "http://localhost:3000/components/button",
|
|
56
|
+
darkMode: true,
|
|
110
57
|
engines: ["chromium", "firefox", "webkit"]
|
|
111
58
|
})
|
|
112
59
|
```
|
|
113
60
|
|
|
114
|
-
## Why
|
|
61
|
+
## Why frameshot?
|
|
115
62
|
|
|
116
63
|
| | Storybook | frameshot |
|
|
117
64
|
|---|-----------|-----------|
|
|
118
|
-
| Setup |
|
|
119
|
-
| Speed |
|
|
120
|
-
| Cross-browser | Chromatic ($149+/mo) | **Free**
|
|
121
|
-
| AI-native |
|
|
122
|
-
| Auth required? | Need full app context | **No** — isolated component render |
|
|
123
|
-
|
|
124
|
-
frameshot is not a Storybook replacement. Storybook is for documentation and design systems. frameshot is for **seeing what you just wrote, right now**.
|
|
65
|
+
| Setup | .stories.tsx + addons + server | **Zero** |
|
|
66
|
+
| Speed | Dev server startup | **~120ms** |
|
|
67
|
+
| Cross-browser | Chromatic ($149+/mo) | **Free** |
|
|
68
|
+
| AI-native | Needs pre-written stories | **Any code snippet** |
|
|
125
69
|
|
|
126
70
|
## Performance
|
|
127
71
|
|
|
128
72
|
| Scenario | Time |
|
|
129
73
|
|----------|------|
|
|
130
|
-
| Warm render
|
|
131
|
-
| Cold start
|
|
132
|
-
|
|
|
133
|
-
|
|
134
|
-
The browser pool stays warm with Tailwind pre-cached. After cold start, renders are **sub-200ms**.
|
|
135
|
-
|
|
136
|
-
## Framework Support
|
|
137
|
-
|
|
138
|
-
### React (JSX + Tailwind)
|
|
139
|
-
```jsx
|
|
140
|
-
function App() {
|
|
141
|
-
const [count, setCount] = React.useState(0)
|
|
142
|
-
return <button onClick={() => setCount(c => c+1)} className="btn">{count}</button>
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### Vue 3 (Composition API + Tailwind)
|
|
147
|
-
```javascript
|
|
148
|
-
const App = {
|
|
149
|
-
setup() {
|
|
150
|
-
const count = ref(0)
|
|
151
|
-
return { count }
|
|
152
|
-
},
|
|
153
|
-
template: `<button @click="count++">{{ count }}</button>`
|
|
154
|
-
}
|
|
155
|
-
```
|
|
74
|
+
| Warm render | **~120ms** |
|
|
75
|
+
| Cold start | ~4s |
|
|
76
|
+
| 3 engines parallel | ~300ms |
|
|
156
77
|
|
|
157
|
-
|
|
158
|
-
```html
|
|
159
|
-
<div class="flex gap-4 p-8">
|
|
160
|
-
<button class="px-4 py-2 bg-blue-500 text-white rounded">Click me</button>
|
|
161
|
-
</div>
|
|
162
|
-
```
|
|
78
|
+
Browser pool stays warm. Tailwind pre-cached. Sub-200ms after first run.
|
|
163
79
|
|
|
164
|
-
##
|
|
80
|
+
## Recipes
|
|
165
81
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
| `FRAMESHOT_BROWSER_PATH` | Custom Chromium executable path |
|
|
82
|
+
- [Claude Code skill setup](examples/claude-code-skill.md) — Auto-preview components with `/project:preview`
|
|
83
|
+
- [Cursor rules](examples/cursor-rules.md) — Auto-verify UI on every edit
|
|
169
84
|
|
|
170
85
|
## Development
|
|
171
86
|
|
|
172
87
|
```bash
|
|
173
|
-
git clone https://github.com/kamegoro/frameshot.git
|
|
174
|
-
|
|
175
|
-
npm install
|
|
176
|
-
npx playwright install chromium
|
|
177
|
-
npm run build
|
|
88
|
+
git clone https://github.com/kamegoro/frameshot.git && cd frameshot
|
|
89
|
+
npm install && npx playwright install chromium && npm run build
|
|
178
90
|
```
|
|
179
91
|
|
|
180
92
|
## License
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
+
import { execSync } from "child_process";
|
|
9
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
12
|
|
|
@@ -14524,7 +14525,9 @@ function date4(params) {
|
|
|
14524
14525
|
config(en_default());
|
|
14525
14526
|
|
|
14526
14527
|
// src/renderer.ts
|
|
14528
|
+
import pixelmatch from "pixelmatch";
|
|
14527
14529
|
import { chromium, firefox, webkit } from "playwright";
|
|
14530
|
+
import { PNG } from "pngjs";
|
|
14528
14531
|
var pool = /* @__PURE__ */ new Map();
|
|
14529
14532
|
var ENGINES = ["chromium", "firefox", "webkit"];
|
|
14530
14533
|
async function warmup(engines = ENGINES) {
|
|
@@ -14542,8 +14545,10 @@ async function getSlot(engine) {
|
|
|
14542
14545
|
headless: true,
|
|
14543
14546
|
...engine === "chromium" ? { channel: "chrome" } : {}
|
|
14544
14547
|
});
|
|
14545
|
-
} catch (
|
|
14546
|
-
throw new Error(
|
|
14548
|
+
} catch (_e) {
|
|
14549
|
+
throw new Error(
|
|
14550
|
+
`${engine} is not installed. Run: npx playwright install ${engine}`
|
|
14551
|
+
);
|
|
14547
14552
|
}
|
|
14548
14553
|
const context = await browser.newContext({
|
|
14549
14554
|
viewport: { width: 1280, height: 800 },
|
|
@@ -14558,47 +14563,67 @@ async function getSlot(engine) {
|
|
|
14558
14563
|
pool.set(engine, slot);
|
|
14559
14564
|
return slot;
|
|
14560
14565
|
}
|
|
14561
|
-
function wrapComponent(code, framework) {
|
|
14566
|
+
function wrapComponent(code, framework, darkMode = false, css = "") {
|
|
14562
14567
|
const tailwind = '<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>';
|
|
14568
|
+
const tailwindConfig = `<script>tailwind.config={darkMode:'class'}</script>`;
|
|
14569
|
+
const customCss = css ? `<style>${css}</style>` : "";
|
|
14563
14570
|
const baseStyle = `<style>*{margin:0;box-sizing:border-box}body{padding:16px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}</style>`;
|
|
14571
|
+
const htmlClass = darkMode ? ' class="dark"' : "";
|
|
14564
14572
|
if (framework === "html") {
|
|
14565
14573
|
if (code.includes("<html")) return code;
|
|
14566
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8">${tailwind}${baseStyle}</head><body>${code}</body></html>`;
|
|
14574
|
+
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}${baseStyle}${customCss}</head><body>${code}</body></html>`;
|
|
14567
14575
|
}
|
|
14568
14576
|
if (framework === "react") {
|
|
14569
|
-
|
|
14577
|
+
const cleanedCode = code.replace(/['"]use client['"];?\n?/g, "").replace(/['"]use server['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/image['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/link['"];?\n?/g, "");
|
|
14578
|
+
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
14570
14579
|
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
14571
14580
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
14572
14581
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
14573
|
-
${baseStyle}</head><body><div id="root"></div>
|
|
14582
|
+
${baseStyle}${customCss}</head><body><div id="root"></div>
|
|
14574
14583
|
<script type="text/babel">
|
|
14575
|
-
|
|
14584
|
+
const Image = (props) => React.createElement('img', {...props, src: props.src?.src || props.src});
|
|
14585
|
+
const Link = ({href, children, ...props}) => React.createElement('a', {href, ...props}, children);
|
|
14586
|
+
${cleanedCode}
|
|
14576
14587
|
const _C = typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:typeof Default!=='undefined'?Default:null;
|
|
14577
14588
|
if(_C)ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(_C));
|
|
14578
14589
|
</script></body></html>`;
|
|
14579
14590
|
}
|
|
14580
14591
|
if (framework === "vue") {
|
|
14581
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8">${tailwind}
|
|
14592
|
+
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
14582
14593
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
14583
|
-
${baseStyle}</head><body><div id="app"></div>
|
|
14594
|
+
${baseStyle}${customCss}</head><body><div id="app"></div>
|
|
14584
14595
|
<script>
|
|
14585
14596
|
const{createApp,ref,reactive,computed,onMounted,watch,watchEffect}=Vue;
|
|
14586
14597
|
${code}
|
|
14587
14598
|
const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
|
|
14588
14599
|
if(_C)createApp(_C).mount('#app');
|
|
14600
|
+
</script></body></html>`;
|
|
14601
|
+
}
|
|
14602
|
+
if (framework === "svelte") {
|
|
14603
|
+
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
14604
|
+
<script src="https://unpkg.com/svelte@4/compiler.cjs"></script>
|
|
14605
|
+
${baseStyle}${customCss}</head><body><div id="app"></div>
|
|
14606
|
+
<script type="module">
|
|
14607
|
+
import "https://unpkg.com/svelte@4/internal/index.mjs";
|
|
14608
|
+
${code}
|
|
14609
|
+
const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
|
|
14610
|
+
if(_C)new _C({target:document.getElementById('app')});
|
|
14589
14611
|
</script></body></html>`;
|
|
14590
14612
|
}
|
|
14591
14613
|
return code;
|
|
14592
14614
|
}
|
|
14593
14615
|
async function renderSingle(engine, html, options) {
|
|
14594
|
-
const {
|
|
14595
|
-
width = 1280,
|
|
14596
|
-
height = 800,
|
|
14597
|
-
fullPage = true,
|
|
14598
|
-
waitFor = 0
|
|
14599
|
-
} = options;
|
|
14616
|
+
const { width = 1280, height = 800, fullPage = true, waitFor = 0 } = options;
|
|
14600
14617
|
const slot = await getSlot(engine);
|
|
14601
14618
|
const { page } = slot;
|
|
14619
|
+
const consoleErrors = [];
|
|
14620
|
+
const onError = (msg) => {
|
|
14621
|
+
if (msg.type() === "error") {
|
|
14622
|
+
consoleErrors.push(msg.text());
|
|
14623
|
+
}
|
|
14624
|
+
};
|
|
14625
|
+
page.on("console", onError);
|
|
14626
|
+
page.on("pageerror", (err) => consoleErrors.push(err.message));
|
|
14602
14627
|
const currentViewport = page.viewportSize();
|
|
14603
14628
|
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14604
14629
|
await page.setViewportSize({ width, height });
|
|
@@ -14612,16 +14637,23 @@ async function renderSingle(engine, html, options) {
|
|
|
14612
14637
|
w: document.documentElement.scrollWidth,
|
|
14613
14638
|
h: document.documentElement.scrollHeight
|
|
14614
14639
|
}));
|
|
14640
|
+
page.removeListener("console", onError);
|
|
14615
14641
|
return {
|
|
14616
14642
|
engine,
|
|
14617
14643
|
image: screenshot.toString("base64"),
|
|
14618
14644
|
width: metrics.w,
|
|
14619
|
-
height: metrics.h
|
|
14645
|
+
height: metrics.h,
|
|
14646
|
+
consoleErrors
|
|
14620
14647
|
};
|
|
14621
14648
|
}
|
|
14622
14649
|
async function render(code, framework, options = {}) {
|
|
14623
14650
|
const engines = options.engines ?? ["chromium"];
|
|
14624
|
-
const html = wrapComponent(
|
|
14651
|
+
const html = wrapComponent(
|
|
14652
|
+
code,
|
|
14653
|
+
framework,
|
|
14654
|
+
options.darkMode ?? false,
|
|
14655
|
+
options.css ?? ""
|
|
14656
|
+
);
|
|
14625
14657
|
const results = await Promise.all(
|
|
14626
14658
|
engines.map((e) => renderSingle(e, html, options))
|
|
14627
14659
|
);
|
|
@@ -14631,9 +14663,22 @@ async function screenshotUrl(url2, options = {}) {
|
|
|
14631
14663
|
const engines = options.engines ?? ["chromium"];
|
|
14632
14664
|
const results = await Promise.all(
|
|
14633
14665
|
engines.map(async (engine) => {
|
|
14634
|
-
const {
|
|
14666
|
+
const {
|
|
14667
|
+
width = 1280,
|
|
14668
|
+
height = 800,
|
|
14669
|
+
fullPage = true,
|
|
14670
|
+
waitFor = 0
|
|
14671
|
+
} = options;
|
|
14635
14672
|
const slot = await getSlot(engine);
|
|
14636
14673
|
const { page } = slot;
|
|
14674
|
+
const consoleErrors = [];
|
|
14675
|
+
const onError = (msg) => {
|
|
14676
|
+
if (msg.type() === "error") {
|
|
14677
|
+
consoleErrors.push(msg.text());
|
|
14678
|
+
}
|
|
14679
|
+
};
|
|
14680
|
+
page.on("console", onError);
|
|
14681
|
+
page.on("pageerror", (err) => consoleErrors.push(err.message));
|
|
14637
14682
|
const currentViewport = page.viewportSize();
|
|
14638
14683
|
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14639
14684
|
await page.setViewportSize({ width, height });
|
|
@@ -14649,11 +14694,137 @@ async function screenshotUrl(url2, options = {}) {
|
|
|
14649
14694
|
w: document.documentElement.scrollWidth,
|
|
14650
14695
|
h: document.documentElement.scrollHeight
|
|
14651
14696
|
}));
|
|
14652
|
-
|
|
14697
|
+
page.removeListener("console", onError);
|
|
14698
|
+
return {
|
|
14699
|
+
engine,
|
|
14700
|
+
image: screenshot.toString("base64"),
|
|
14701
|
+
width: metrics.w,
|
|
14702
|
+
height: metrics.h,
|
|
14703
|
+
consoleErrors
|
|
14704
|
+
};
|
|
14653
14705
|
})
|
|
14654
14706
|
);
|
|
14655
14707
|
return results;
|
|
14656
14708
|
}
|
|
14709
|
+
async function auditA11y(code, framework, options = {}) {
|
|
14710
|
+
const html = wrapComponent(
|
|
14711
|
+
code,
|
|
14712
|
+
framework,
|
|
14713
|
+
options.darkMode ?? false,
|
|
14714
|
+
options.css ?? ""
|
|
14715
|
+
);
|
|
14716
|
+
const slot = await getSlot("chromium");
|
|
14717
|
+
const { page } = slot;
|
|
14718
|
+
const { width = 1280, height = 800 } = options;
|
|
14719
|
+
const currentViewport = page.viewportSize();
|
|
14720
|
+
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14721
|
+
await page.setViewportSize({ width, height });
|
|
14722
|
+
}
|
|
14723
|
+
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
14724
|
+
const axeSource = await import("axe-core").then((m) => m.source);
|
|
14725
|
+
await page.addScriptTag({ content: axeSource });
|
|
14726
|
+
const results = await page.evaluate(() => {
|
|
14727
|
+
return window.axe.run();
|
|
14728
|
+
});
|
|
14729
|
+
return {
|
|
14730
|
+
violations: results.violations.map(
|
|
14731
|
+
(v) => ({
|
|
14732
|
+
id: v.id,
|
|
14733
|
+
impact: v.impact,
|
|
14734
|
+
description: v.description,
|
|
14735
|
+
helpUrl: v.helpUrl,
|
|
14736
|
+
nodes: v.nodes.map((n) => ({
|
|
14737
|
+
html: n.html,
|
|
14738
|
+
target: n.target
|
|
14739
|
+
}))
|
|
14740
|
+
})
|
|
14741
|
+
),
|
|
14742
|
+
passes: results.passes.length,
|
|
14743
|
+
incomplete: results.incomplete.length
|
|
14744
|
+
};
|
|
14745
|
+
}
|
|
14746
|
+
async function diffComponent(before, after, framework, options = {}) {
|
|
14747
|
+
const [beforeResults, afterResults] = await Promise.all([
|
|
14748
|
+
render(before, framework, { ...options, engines: ["chromium"] }),
|
|
14749
|
+
render(after, framework, { ...options, engines: ["chromium"] })
|
|
14750
|
+
]);
|
|
14751
|
+
const beforeBuf = Buffer.from(beforeResults[0].image, "base64");
|
|
14752
|
+
const afterBuf = Buffer.from(afterResults[0].image, "base64");
|
|
14753
|
+
const beforePng = PNG.sync.read(beforeBuf);
|
|
14754
|
+
const afterPng = PNG.sync.read(afterBuf);
|
|
14755
|
+
const width = Math.max(beforePng.width, afterPng.width);
|
|
14756
|
+
const height = Math.max(beforePng.height, afterPng.height);
|
|
14757
|
+
const normalizedBefore = new PNG({ width, height });
|
|
14758
|
+
const normalizedAfter = new PNG({ width, height });
|
|
14759
|
+
PNG.bitblt(
|
|
14760
|
+
beforePng,
|
|
14761
|
+
normalizedBefore,
|
|
14762
|
+
0,
|
|
14763
|
+
0,
|
|
14764
|
+
beforePng.width,
|
|
14765
|
+
beforePng.height,
|
|
14766
|
+
0,
|
|
14767
|
+
0
|
|
14768
|
+
);
|
|
14769
|
+
PNG.bitblt(
|
|
14770
|
+
afterPng,
|
|
14771
|
+
normalizedAfter,
|
|
14772
|
+
0,
|
|
14773
|
+
0,
|
|
14774
|
+
afterPng.width,
|
|
14775
|
+
afterPng.height,
|
|
14776
|
+
0,
|
|
14777
|
+
0
|
|
14778
|
+
);
|
|
14779
|
+
const diffPng = new PNG({ width, height });
|
|
14780
|
+
const diffPixels = pixelmatch(
|
|
14781
|
+
normalizedBefore.data,
|
|
14782
|
+
normalizedAfter.data,
|
|
14783
|
+
diffPng.data,
|
|
14784
|
+
width,
|
|
14785
|
+
height,
|
|
14786
|
+
{ threshold: 0.1 }
|
|
14787
|
+
);
|
|
14788
|
+
const totalPixels = width * height;
|
|
14789
|
+
return {
|
|
14790
|
+
before: beforeResults[0].image,
|
|
14791
|
+
after: afterResults[0].image,
|
|
14792
|
+
diff: PNG.sync.write(diffPng).toString("base64"),
|
|
14793
|
+
diffPixels,
|
|
14794
|
+
totalPixels,
|
|
14795
|
+
diffPercentage: Math.round(diffPixels / totalPixels * 1e4) / 100
|
|
14796
|
+
};
|
|
14797
|
+
}
|
|
14798
|
+
async function captureAnimation(code, framework, options = {}) {
|
|
14799
|
+
const { frames = 5, duration: duration3 = 1e3 } = options;
|
|
14800
|
+
const html = wrapComponent(
|
|
14801
|
+
code,
|
|
14802
|
+
framework,
|
|
14803
|
+
options.darkMode ?? false,
|
|
14804
|
+
options.css ?? ""
|
|
14805
|
+
);
|
|
14806
|
+
const slot = await getSlot("chromium");
|
|
14807
|
+
const { page } = slot;
|
|
14808
|
+
const { width = 1280, height = 800 } = options;
|
|
14809
|
+
const currentViewport = page.viewportSize();
|
|
14810
|
+
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14811
|
+
await page.setViewportSize({ width, height });
|
|
14812
|
+
}
|
|
14813
|
+
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
14814
|
+
const interval = duration3 / (frames - 1);
|
|
14815
|
+
const results = [];
|
|
14816
|
+
for (let i = 0; i < frames; i++) {
|
|
14817
|
+
if (i > 0) {
|
|
14818
|
+
await page.waitForTimeout(interval);
|
|
14819
|
+
}
|
|
14820
|
+
const screenshot = await page.screenshot({ type: "png", fullPage: false });
|
|
14821
|
+
results.push({
|
|
14822
|
+
timestamp: Math.round(i * interval),
|
|
14823
|
+
image: screenshot.toString("base64")
|
|
14824
|
+
});
|
|
14825
|
+
}
|
|
14826
|
+
return results;
|
|
14827
|
+
}
|
|
14657
14828
|
async function shutdown() {
|
|
14658
14829
|
for (const slot of pool.values()) {
|
|
14659
14830
|
await slot.browser.close().catch(() => {
|
|
@@ -14663,6 +14834,18 @@ async function shutdown() {
|
|
|
14663
14834
|
}
|
|
14664
14835
|
|
|
14665
14836
|
// src/index.ts
|
|
14837
|
+
async function ensureBrowser() {
|
|
14838
|
+
try {
|
|
14839
|
+
await warmup(["chromium"]);
|
|
14840
|
+
} catch {
|
|
14841
|
+
try {
|
|
14842
|
+
execSync("npx playwright install chromium", { stdio: "pipe" });
|
|
14843
|
+
await warmup(["chromium"]);
|
|
14844
|
+
} catch {
|
|
14845
|
+
}
|
|
14846
|
+
}
|
|
14847
|
+
}
|
|
14848
|
+
await ensureBrowser();
|
|
14666
14849
|
var server = new McpServer({
|
|
14667
14850
|
name: "frameshot",
|
|
14668
14851
|
version: "0.1.0"
|
|
@@ -14672,20 +14855,47 @@ server.tool(
|
|
|
14672
14855
|
"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
14856
|
{
|
|
14674
14857
|
code: external_exports.string().describe("Component code to render"),
|
|
14675
|
-
framework: external_exports.enum(["html", "react", "vue"]).default("react").describe("Framework: html, react, or
|
|
14676
|
-
engines: external_exports.array(external_exports.enum(["chromium", "firefox", "webkit"])).optional().default(["chromium"]).describe(
|
|
14858
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
14859
|
+
engines: external_exports.array(external_exports.enum(["chromium", "firefox", "webkit"])).optional().default(["chromium"]).describe(
|
|
14860
|
+
'Browser engines to render in. Use ["chromium","firefox","webkit"] for cross-browser check.'
|
|
14861
|
+
),
|
|
14677
14862
|
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14678
14863
|
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
14679
|
-
fullPage: external_exports.boolean().optional().default(true).describe("Capture full scroll height")
|
|
14864
|
+
fullPage: external_exports.boolean().optional().default(true).describe("Capture full scroll height"),
|
|
14865
|
+
darkMode: external_exports.boolean().optional().default(false).describe("Render with Tailwind dark mode (adds 'dark' class to html)"),
|
|
14866
|
+
colorSchemes: external_exports.array(external_exports.enum(["light", "dark"])).optional().describe(
|
|
14867
|
+
'Render both: ["light","dark"] returns 2 screenshots for comparison'
|
|
14868
|
+
),
|
|
14869
|
+
css: external_exports.string().optional().describe("Custom CSS to inject (design tokens, variables, etc)")
|
|
14680
14870
|
},
|
|
14681
|
-
async ({
|
|
14871
|
+
async ({
|
|
14872
|
+
code,
|
|
14873
|
+
framework,
|
|
14874
|
+
engines,
|
|
14875
|
+
width,
|
|
14876
|
+
height,
|
|
14877
|
+
fullPage,
|
|
14878
|
+
darkMode,
|
|
14879
|
+
colorSchemes,
|
|
14880
|
+
css
|
|
14881
|
+
}) => {
|
|
14682
14882
|
try {
|
|
14683
|
-
const
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14883
|
+
const start = performance.now();
|
|
14884
|
+
const schemes = colorSchemes ?? (darkMode ? ["dark"] : ["light"]);
|
|
14885
|
+
const allResults = await Promise.all(
|
|
14886
|
+
schemes.map(
|
|
14887
|
+
(scheme) => render(code, framework, {
|
|
14888
|
+
width,
|
|
14889
|
+
height,
|
|
14890
|
+
fullPage,
|
|
14891
|
+
engines,
|
|
14892
|
+
darkMode: scheme === "dark",
|
|
14893
|
+
css
|
|
14894
|
+
})
|
|
14895
|
+
)
|
|
14896
|
+
);
|
|
14897
|
+
const elapsed = Math.round(performance.now() - start);
|
|
14898
|
+
const results = allResults.flat();
|
|
14689
14899
|
const content = results.flatMap((r) => [
|
|
14690
14900
|
{
|
|
14691
14901
|
type: "image",
|
|
@@ -14694,13 +14904,18 @@ server.tool(
|
|
|
14694
14904
|
},
|
|
14695
14905
|
{
|
|
14696
14906
|
type: "text",
|
|
14697
|
-
text: `[${r.engine}] ${r.width}x${r.height}`
|
|
14907
|
+
text: `[${r.engine}] ${r.width}x${r.height} (${elapsed}ms)${r.consoleErrors.length ? `
|
|
14908
|
+
\u26A0\uFE0F Console errors:
|
|
14909
|
+
${r.consoleErrors.join("\n")}` : ""}`
|
|
14698
14910
|
}
|
|
14699
14911
|
]);
|
|
14700
14912
|
return { content };
|
|
14701
14913
|
} catch (error51) {
|
|
14702
14914
|
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14703
|
-
return {
|
|
14915
|
+
return {
|
|
14916
|
+
content: [{ type: "text", text: `Render failed: ${msg}` }],
|
|
14917
|
+
isError: true
|
|
14918
|
+
};
|
|
14704
14919
|
}
|
|
14705
14920
|
}
|
|
14706
14921
|
);
|
|
@@ -14730,19 +14945,276 @@ server.tool(
|
|
|
14730
14945
|
},
|
|
14731
14946
|
{
|
|
14732
14947
|
type: "text",
|
|
14733
|
-
text: `[${r.engine}] ${r.width}x${r.height}`
|
|
14948
|
+
text: `[${r.engine}] ${r.width}x${r.height}${r.consoleErrors.length ? `
|
|
14949
|
+
\u26A0\uFE0F Console errors:
|
|
14950
|
+
${r.consoleErrors.join("\n")}` : ""}`
|
|
14734
14951
|
}
|
|
14735
14952
|
]);
|
|
14736
14953
|
return { content };
|
|
14737
14954
|
} catch (error51) {
|
|
14738
14955
|
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14739
|
-
return {
|
|
14956
|
+
return {
|
|
14957
|
+
content: [{ type: "text", text: `Screenshot failed: ${msg}` }],
|
|
14958
|
+
isError: true
|
|
14959
|
+
};
|
|
14960
|
+
}
|
|
14961
|
+
}
|
|
14962
|
+
);
|
|
14963
|
+
var DEVICE_PRESETS = {
|
|
14964
|
+
mobile: { width: 375, height: 667 },
|
|
14965
|
+
tablet: { width: 768, height: 1024 },
|
|
14966
|
+
desktop: { width: 1280, height: 800 }
|
|
14967
|
+
};
|
|
14968
|
+
server.tool(
|
|
14969
|
+
"render_responsive",
|
|
14970
|
+
"Render a component at mobile, tablet, and desktop sizes in one call. Returns 3 screenshots for responsive verification.",
|
|
14971
|
+
{
|
|
14972
|
+
code: external_exports.string().describe("Component code to render"),
|
|
14973
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
14974
|
+
devices: external_exports.array(external_exports.enum(["mobile", "tablet", "desktop"])).optional().default(["mobile", "tablet", "desktop"]).describe("Device sizes to render")
|
|
14975
|
+
},
|
|
14976
|
+
async ({ code, framework, devices }) => {
|
|
14977
|
+
try {
|
|
14978
|
+
const results = await Promise.all(
|
|
14979
|
+
devices.map(async (device) => {
|
|
14980
|
+
const preset = DEVICE_PRESETS[device];
|
|
14981
|
+
const [result] = await render(code, framework, {
|
|
14982
|
+
width: preset.width,
|
|
14983
|
+
height: preset.height,
|
|
14984
|
+
fullPage: true,
|
|
14985
|
+
engines: ["chromium"]
|
|
14986
|
+
});
|
|
14987
|
+
return { device, ...result };
|
|
14988
|
+
})
|
|
14989
|
+
);
|
|
14990
|
+
const content = results.flatMap((r) => [
|
|
14991
|
+
{
|
|
14992
|
+
type: "image",
|
|
14993
|
+
data: r.image,
|
|
14994
|
+
mimeType: "image/png"
|
|
14995
|
+
},
|
|
14996
|
+
{
|
|
14997
|
+
type: "text",
|
|
14998
|
+
text: `[${r.device}] ${r.width}x${r.height}`
|
|
14999
|
+
}
|
|
15000
|
+
]);
|
|
15001
|
+
return { content };
|
|
15002
|
+
} catch (error51) {
|
|
15003
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15004
|
+
return {
|
|
15005
|
+
content: [
|
|
15006
|
+
{ type: "text", text: `Responsive render failed: ${msg}` }
|
|
15007
|
+
],
|
|
15008
|
+
isError: true
|
|
15009
|
+
};
|
|
15010
|
+
}
|
|
15011
|
+
}
|
|
15012
|
+
);
|
|
15013
|
+
server.tool(
|
|
15014
|
+
"render_variants",
|
|
15015
|
+
"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.",
|
|
15016
|
+
{
|
|
15017
|
+
code: external_exports.string().describe("Component code (must export a function that accepts props)"),
|
|
15018
|
+
variants: external_exports.array(
|
|
15019
|
+
external_exports.object({
|
|
15020
|
+
label: external_exports.string().describe("Label for this variant (e.g. 'disabled', 'loading')"),
|
|
15021
|
+
props: external_exports.string().describe("Props as JSON string to pass to the component")
|
|
15022
|
+
})
|
|
15023
|
+
).describe("Array of variants to render"),
|
|
15024
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
15025
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15026
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15027
|
+
},
|
|
15028
|
+
async ({ code, variants, framework, width, height }) => {
|
|
15029
|
+
try {
|
|
15030
|
+
const results = await Promise.all(
|
|
15031
|
+
variants.map(async (variant) => {
|
|
15032
|
+
const wrappedCode = framework === "react" ? `${code}
|
|
15033
|
+
const _VARIANT_PROPS = ${variant.props};
|
|
15034
|
+
function _VariantWrapper() { return <App {..._VARIANT_PROPS} />; }` : code;
|
|
15035
|
+
const renderFramework = framework;
|
|
15036
|
+
const overrideCode = framework === "react" ? wrappedCode.replace(
|
|
15037
|
+
/const _C = typeof App/,
|
|
15038
|
+
"const _C = typeof _VariantWrapper!=='undefined'?_VariantWrapper:typeof App"
|
|
15039
|
+
) : wrappedCode;
|
|
15040
|
+
const [result] = await render(overrideCode, renderFramework, {
|
|
15041
|
+
width,
|
|
15042
|
+
height,
|
|
15043
|
+
fullPage: true,
|
|
15044
|
+
engines: ["chromium"]
|
|
15045
|
+
});
|
|
15046
|
+
return { label: variant.label, ...result };
|
|
15047
|
+
})
|
|
15048
|
+
);
|
|
15049
|
+
const content = results.flatMap((r) => [
|
|
15050
|
+
{
|
|
15051
|
+
type: "image",
|
|
15052
|
+
data: r.image,
|
|
15053
|
+
mimeType: "image/png"
|
|
15054
|
+
},
|
|
15055
|
+
{
|
|
15056
|
+
type: "text",
|
|
15057
|
+
text: `[${r.label}] ${r.width}x${r.height}${r.consoleErrors.length ? `
|
|
15058
|
+
\u26A0\uFE0F Console errors:
|
|
15059
|
+
${r.consoleErrors.join("\n")}` : ""}`
|
|
15060
|
+
}
|
|
15061
|
+
]);
|
|
15062
|
+
return { content };
|
|
15063
|
+
} catch (error51) {
|
|
15064
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15065
|
+
return {
|
|
15066
|
+
content: [
|
|
15067
|
+
{ type: "text", text: `Variants render failed: ${msg}` }
|
|
15068
|
+
],
|
|
15069
|
+
isError: true
|
|
15070
|
+
};
|
|
15071
|
+
}
|
|
15072
|
+
}
|
|
15073
|
+
);
|
|
15074
|
+
server.tool(
|
|
15075
|
+
"audit_a11y",
|
|
15076
|
+
"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.",
|
|
15077
|
+
{
|
|
15078
|
+
code: external_exports.string().describe("Component code to audit"),
|
|
15079
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
15080
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15081
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15082
|
+
},
|
|
15083
|
+
async ({ code, framework, width, height }) => {
|
|
15084
|
+
try {
|
|
15085
|
+
const result = await auditA11y(code, framework, { width, height });
|
|
15086
|
+
if (result.violations.length === 0) {
|
|
15087
|
+
return {
|
|
15088
|
+
content: [
|
|
15089
|
+
{
|
|
15090
|
+
type: "text",
|
|
15091
|
+
text: `\u2705 No accessibility violations found. (${result.passes} rules passed, ${result.incomplete} need review)`
|
|
15092
|
+
}
|
|
15093
|
+
]
|
|
15094
|
+
};
|
|
15095
|
+
}
|
|
15096
|
+
const report = result.violations.map((v) => {
|
|
15097
|
+
const nodes = v.nodes.slice(0, 3).map((n) => ` ${n.target.join(" > ")}
|
|
15098
|
+
${n.html}`).join("\n");
|
|
15099
|
+
return `[${v.impact?.toUpperCase()}] ${v.id}
|
|
15100
|
+
${v.description}
|
|
15101
|
+
${v.helpUrl}
|
|
15102
|
+
${nodes}`;
|
|
15103
|
+
}).join("\n\n");
|
|
15104
|
+
return {
|
|
15105
|
+
content: [
|
|
15106
|
+
{
|
|
15107
|
+
type: "text",
|
|
15108
|
+
text: `Found ${result.violations.length} accessibility violation(s):
|
|
15109
|
+
|
|
15110
|
+
${report}
|
|
15111
|
+
|
|
15112
|
+
(${result.passes} rules passed, ${result.incomplete} need review)`
|
|
15113
|
+
}
|
|
15114
|
+
]
|
|
15115
|
+
};
|
|
15116
|
+
} catch (error51) {
|
|
15117
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15118
|
+
return {
|
|
15119
|
+
content: [{ type: "text", text: `A11y audit failed: ${msg}` }],
|
|
15120
|
+
isError: true
|
|
15121
|
+
};
|
|
15122
|
+
}
|
|
15123
|
+
}
|
|
15124
|
+
);
|
|
15125
|
+
server.tool(
|
|
15126
|
+
"diff_component",
|
|
15127
|
+
"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.",
|
|
15128
|
+
{
|
|
15129
|
+
before: external_exports.string().describe("Component code BEFORE the change"),
|
|
15130
|
+
after: external_exports.string().describe("Component code AFTER the change"),
|
|
15131
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
15132
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15133
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15134
|
+
},
|
|
15135
|
+
async ({ before, after, framework, width, height }) => {
|
|
15136
|
+
try {
|
|
15137
|
+
const result = await diffComponent(before, after, framework, {
|
|
15138
|
+
width,
|
|
15139
|
+
height
|
|
15140
|
+
});
|
|
15141
|
+
const content = [
|
|
15142
|
+
{
|
|
15143
|
+
type: "text",
|
|
15144
|
+
text: `Visual diff: ${result.diffPercentage}% pixels changed (${result.diffPixels}/${result.totalPixels})`
|
|
15145
|
+
},
|
|
15146
|
+
{ type: "text", text: "Before:" },
|
|
15147
|
+
{
|
|
15148
|
+
type: "image",
|
|
15149
|
+
data: result.before,
|
|
15150
|
+
mimeType: "image/png"
|
|
15151
|
+
},
|
|
15152
|
+
{ type: "text", text: "After:" },
|
|
15153
|
+
{
|
|
15154
|
+
type: "image",
|
|
15155
|
+
data: result.after,
|
|
15156
|
+
mimeType: "image/png"
|
|
15157
|
+
},
|
|
15158
|
+
{ type: "text", text: "Diff (red = changed pixels):" },
|
|
15159
|
+
{
|
|
15160
|
+
type: "image",
|
|
15161
|
+
data: result.diff,
|
|
15162
|
+
mimeType: "image/png"
|
|
15163
|
+
}
|
|
15164
|
+
];
|
|
15165
|
+
return { content };
|
|
15166
|
+
} catch (error51) {
|
|
15167
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15168
|
+
return {
|
|
15169
|
+
content: [{ type: "text", text: `Diff failed: ${msg}` }],
|
|
15170
|
+
isError: true
|
|
15171
|
+
};
|
|
15172
|
+
}
|
|
15173
|
+
}
|
|
15174
|
+
);
|
|
15175
|
+
server.tool(
|
|
15176
|
+
"capture_animation",
|
|
15177
|
+
"Capture multiple frames of a CSS animation or transition over time. Returns sequential screenshots to verify animation behavior, timing, and smoothness.",
|
|
15178
|
+
{
|
|
15179
|
+
code: external_exports.string().describe("Component code with CSS animations/transitions"),
|
|
15180
|
+
framework: external_exports.enum(["html", "react", "vue", "svelte"]).default("react").describe("Framework: html, react, vue, or svelte"),
|
|
15181
|
+
frames: external_exports.number().optional().default(5).describe("Number of frames to capture"),
|
|
15182
|
+
duration: external_exports.number().optional().default(1e3).describe("Total capture duration in ms"),
|
|
15183
|
+
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
15184
|
+
height: external_exports.number().optional().default(800).describe("Viewport height (px)")
|
|
15185
|
+
},
|
|
15186
|
+
async ({ code, framework, frames, duration: duration3, width, height }) => {
|
|
15187
|
+
try {
|
|
15188
|
+
const results = await captureAnimation(code, framework, {
|
|
15189
|
+
frames,
|
|
15190
|
+
duration: duration3,
|
|
15191
|
+
width,
|
|
15192
|
+
height
|
|
15193
|
+
});
|
|
15194
|
+
const content = results.flatMap((r) => [
|
|
15195
|
+
{
|
|
15196
|
+
type: "image",
|
|
15197
|
+
data: r.image,
|
|
15198
|
+
mimeType: "image/png"
|
|
15199
|
+
},
|
|
15200
|
+
{
|
|
15201
|
+
type: "text",
|
|
15202
|
+
text: `[${r.timestamp}ms]`
|
|
15203
|
+
}
|
|
15204
|
+
]);
|
|
15205
|
+
return { content };
|
|
15206
|
+
} catch (error51) {
|
|
15207
|
+
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
15208
|
+
return {
|
|
15209
|
+
content: [
|
|
15210
|
+
{ type: "text", text: `Animation capture failed: ${msg}` }
|
|
15211
|
+
],
|
|
15212
|
+
isError: true
|
|
15213
|
+
};
|
|
14740
15214
|
}
|
|
14741
15215
|
}
|
|
14742
15216
|
);
|
|
14743
15217
|
var transport = new StdioServerTransport();
|
|
14744
|
-
warmup(["chromium"]).catch(() => {
|
|
14745
|
-
});
|
|
14746
15218
|
await server.connect(transport);
|
|
14747
15219
|
process.on("SIGINT", async () => {
|
|
14748
15220
|
await shutdown();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frameshot-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"dev": "tsup src/index.ts --format esm --watch",
|
|
18
18
|
"start": "node dist/index.js",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "biome check src/",
|
|
21
|
+
"lint:fix": "biome check --write src/",
|
|
22
|
+
"format": "biome format --write src/",
|
|
20
23
|
"prepublishOnly": "npm run build"
|
|
21
24
|
},
|
|
22
25
|
"keywords": [
|
|
@@ -49,10 +52,15 @@
|
|
|
49
52
|
},
|
|
50
53
|
"dependencies": {
|
|
51
54
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
52
|
-
"
|
|
55
|
+
"axe-core": "^4.12.1",
|
|
56
|
+
"pixelmatch": "^7.2.0",
|
|
57
|
+
"playwright": "^1.52.0",
|
|
58
|
+
"pngjs": "^7.0.0"
|
|
53
59
|
},
|
|
54
60
|
"devDependencies": {
|
|
61
|
+
"@biomejs/biome": "^2.5.0",
|
|
55
62
|
"@types/node": "^22.0.0",
|
|
63
|
+
"@types/pngjs": "^6.0.5",
|
|
56
64
|
"tsup": "^8.0.0",
|
|
57
65
|
"typescript": "^5.7.0"
|
|
58
66
|
},
|