frameshot-mcp 0.1.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 +39 -125
- package/dist/index.js +512 -38
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -1,57 +1,24 @@
|
|
|
1
1
|
# frameshot
|
|
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
|
-
You're building UI with an AI agent. The agent writes a component but can't see it. You have to:
|
|
10
|
-
1. Start the dev server
|
|
11
|
-
2. Log in
|
|
12
|
-
3. Navigate to the right page
|
|
13
|
-
4. Scroll to the component
|
|
14
|
-
5. Tell the agent what's wrong
|
|
15
|
-
|
|
16
|
-
**frameshot** skips all of that. The agent renders the component itself, sees the result, and fixes it.
|
|
17
|
-
|
|
18
|
-
## Quick Start
|
|
19
|
-
|
|
20
|
-
### Claude Code
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
claude mcp add frameshot -- npx frameshot-mcp@latest
|
|
24
10
|
```
|
|
25
|
-
|
|
26
|
-
That's it. Now your AI agent can see UI.
|
|
27
|
-
|
|
28
|
-
### Cursor (`.cursor/mcp.json`)
|
|
29
|
-
|
|
30
|
-
```json
|
|
31
|
-
{
|
|
32
|
-
"mcpServers": {
|
|
33
|
-
"frameshot": {
|
|
34
|
-
"command": "npx",
|
|
35
|
-
"args": ["frameshot-mcp@latest"]
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
11
|
+
AI writes code → AI calls frameshot → AI sees the result → AI self-corrects
|
|
39
12
|
```
|
|
40
13
|
|
|
41
|
-
|
|
14
|
+
## Install
|
|
42
15
|
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
"servers": {
|
|
46
|
-
"frameshot": {
|
|
47
|
-
"command": "npx",
|
|
48
|
-
"args": ["frameshot-mcp@latest"]
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
16
|
+
```bash
|
|
17
|
+
claude mcp add frameshot -- npx frameshot-mcp@latest
|
|
52
18
|
```
|
|
53
19
|
|
|
54
|
-
|
|
20
|
+
<details>
|
|
21
|
+
<summary>Cursor / VS Code / Other</summary>
|
|
55
22
|
|
|
56
23
|
```json
|
|
57
24
|
{
|
|
@@ -64,115 +31,62 @@ That's it. Now your AI agent can see UI.
|
|
|
64
31
|
}
|
|
65
32
|
```
|
|
66
33
|
|
|
67
|
-
>
|
|
34
|
+
</details>
|
|
68
35
|
|
|
69
36
|
## Tools
|
|
70
37
|
|
|
71
|
-
|
|
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). |
|
|
72
47
|
|
|
73
|
-
|
|
48
|
+
### Example
|
|
74
49
|
|
|
75
50
|
```typescript
|
|
76
51
|
render_component({
|
|
77
|
-
code: `
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<div className="p-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl">
|
|
81
|
-
<h1 className="text-4xl font-bold text-white">Hello World</h1>
|
|
82
|
-
</div>
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
`,
|
|
52
|
+
code: `function App() {
|
|
53
|
+
return <div className="p-8 bg-blue-500 text-white rounded-xl">Hello</div>
|
|
54
|
+
}`,
|
|
86
55
|
framework: "react",
|
|
56
|
+
darkMode: true,
|
|
87
57
|
engines: ["chromium", "firefox", "webkit"]
|
|
88
58
|
})
|
|
89
|
-
// Returns 3 screenshots — one per browser engine
|
|
90
59
|
```
|
|
91
60
|
|
|
92
|
-
|
|
93
|
-
|-----------|---------|-------------|
|
|
94
|
-
| `code` | required | Component source code |
|
|
95
|
-
| `framework` | `"react"` | `"react"` · `"vue"` · `"html"` |
|
|
96
|
-
| `engines` | `["chromium"]` | Browser engines to render in |
|
|
97
|
-
| `width` | `1280` | Viewport width |
|
|
98
|
-
| `height` | `800` | Viewport height |
|
|
99
|
-
| `fullPage` | `true` | Capture full scroll height |
|
|
100
|
-
|
|
101
|
-
### `screenshot_url`
|
|
102
|
-
|
|
103
|
-
Screenshot a running app (e.g. localhost) across browsers.
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
screenshot_url({
|
|
107
|
-
url: "http://localhost:3000/components/button",
|
|
108
|
-
engines: ["chromium", "firefox", "webkit"]
|
|
109
|
-
})
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Why Not Storybook?
|
|
61
|
+
## Why frameshot?
|
|
113
62
|
|
|
114
63
|
| | Storybook | frameshot |
|
|
115
64
|
|---|-----------|-----------|
|
|
116
|
-
| Setup |
|
|
117
|
-
| Speed |
|
|
118
|
-
| Cross-browser | Chromatic ($149+/mo) | **Free**
|
|
119
|
-
| AI-native |
|
|
120
|
-
| Auth required? | Need full app context | **No** — isolated component render |
|
|
121
|
-
|
|
122
|
-
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** |
|
|
123
69
|
|
|
124
70
|
## Performance
|
|
125
71
|
|
|
126
72
|
| Scenario | Time |
|
|
127
73
|
|----------|------|
|
|
128
|
-
| Warm render
|
|
129
|
-
| Cold start
|
|
130
|
-
|
|
|
131
|
-
|
|
132
|
-
The browser stays warm between calls. Second render onwards is sub-second.
|
|
74
|
+
| Warm render | **~120ms** |
|
|
75
|
+
| Cold start | ~4s |
|
|
76
|
+
| 3 engines parallel | ~300ms |
|
|
133
77
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
### React (JSX + Tailwind)
|
|
137
|
-
```jsx
|
|
138
|
-
function App() {
|
|
139
|
-
const [count, setCount] = React.useState(0)
|
|
140
|
-
return <button onClick={() => setCount(c => c+1)} className="btn">{count}</button>
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Vue 3 (Composition API + Tailwind)
|
|
145
|
-
```javascript
|
|
146
|
-
const App = {
|
|
147
|
-
setup() {
|
|
148
|
-
const count = ref(0)
|
|
149
|
-
return { count }
|
|
150
|
-
},
|
|
151
|
-
template: `<button @click="count++">{{ count }}</button>`
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### HTML (+ Tailwind)
|
|
156
|
-
```html
|
|
157
|
-
<div class="flex gap-4 p-8">
|
|
158
|
-
<button class="px-4 py-2 bg-blue-500 text-white rounded">Click me</button>
|
|
159
|
-
</div>
|
|
160
|
-
```
|
|
78
|
+
Browser pool stays warm. Tailwind pre-cached. Sub-200ms after first run.
|
|
161
79
|
|
|
162
|
-
##
|
|
80
|
+
## Recipes
|
|
163
81
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
| `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
|
|
167
84
|
|
|
168
85
|
## Development
|
|
169
86
|
|
|
170
87
|
```bash
|
|
171
|
-
git clone https://github.com/kamegoro/frameshot.git
|
|
172
|
-
|
|
173
|
-
npm install
|
|
174
|
-
npx playwright install chromium
|
|
175
|
-
npm run build
|
|
88
|
+
git clone https://github.com/kamegoro/frameshot.git && cd frameshot
|
|
89
|
+
npm install && npx playwright install chromium && npm run build
|
|
176
90
|
```
|
|
177
91
|
|
|
178
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,66 +14545,90 @@ 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 },
|
|
14550
14555
|
deviceScaleFactor: 2
|
|
14551
14556
|
});
|
|
14552
14557
|
const page = await context.newPage();
|
|
14558
|
+
await page.setContent(
|
|
14559
|
+
'<html><head><script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script></head><body></body></html>',
|
|
14560
|
+
{ waitUntil: "networkidle" }
|
|
14561
|
+
);
|
|
14553
14562
|
const slot = { browser, page, ready: true };
|
|
14554
14563
|
pool.set(engine, slot);
|
|
14555
14564
|
return slot;
|
|
14556
14565
|
}
|
|
14557
|
-
function wrapComponent(code, framework) {
|
|
14566
|
+
function wrapComponent(code, framework, darkMode = false, css = "") {
|
|
14558
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>` : "";
|
|
14559
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"' : "";
|
|
14560
14572
|
if (framework === "html") {
|
|
14561
14573
|
if (code.includes("<html")) return code;
|
|
14562
|
-
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>`;
|
|
14563
14575
|
}
|
|
14564
14576
|
if (framework === "react") {
|
|
14565
|
-
|
|
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}
|
|
14566
14579
|
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
14567
14580
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
14568
14581
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
14569
|
-
${baseStyle}</head><body><div id="root"></div>
|
|
14582
|
+
${baseStyle}${customCss}</head><body><div id="root"></div>
|
|
14570
14583
|
<script type="text/babel">
|
|
14571
|
-
|
|
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}
|
|
14572
14587
|
const _C = typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:typeof Default!=='undefined'?Default:null;
|
|
14573
14588
|
if(_C)ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(_C));
|
|
14574
14589
|
</script></body></html>`;
|
|
14575
14590
|
}
|
|
14576
14591
|
if (framework === "vue") {
|
|
14577
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8">${tailwind}
|
|
14592
|
+
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
14578
14593
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
14579
|
-
${baseStyle}</head><body><div id="app"></div>
|
|
14594
|
+
${baseStyle}${customCss}</head><body><div id="app"></div>
|
|
14580
14595
|
<script>
|
|
14581
14596
|
const{createApp,ref,reactive,computed,onMounted,watch,watchEffect}=Vue;
|
|
14582
14597
|
${code}
|
|
14583
14598
|
const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
|
|
14584
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')});
|
|
14585
14611
|
</script></body></html>`;
|
|
14586
14612
|
}
|
|
14587
14613
|
return code;
|
|
14588
14614
|
}
|
|
14589
14615
|
async function renderSingle(engine, html, options) {
|
|
14590
|
-
const {
|
|
14591
|
-
width = 1280,
|
|
14592
|
-
height = 800,
|
|
14593
|
-
fullPage = true,
|
|
14594
|
-
waitFor = 0
|
|
14595
|
-
} = options;
|
|
14616
|
+
const { width = 1280, height = 800, fullPage = true, waitFor = 0 } = options;
|
|
14596
14617
|
const slot = await getSlot(engine);
|
|
14597
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));
|
|
14598
14627
|
const currentViewport = page.viewportSize();
|
|
14599
14628
|
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14600
14629
|
await page.setViewportSize({ width, height });
|
|
14601
14630
|
}
|
|
14602
|
-
await page.setContent(html, { waitUntil: "
|
|
14603
|
-
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
14604
|
-
});
|
|
14631
|
+
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
14605
14632
|
if (waitFor > 0) {
|
|
14606
14633
|
await page.waitForTimeout(waitFor);
|
|
14607
14634
|
}
|
|
@@ -14610,16 +14637,23 @@ async function renderSingle(engine, html, options) {
|
|
|
14610
14637
|
w: document.documentElement.scrollWidth,
|
|
14611
14638
|
h: document.documentElement.scrollHeight
|
|
14612
14639
|
}));
|
|
14640
|
+
page.removeListener("console", onError);
|
|
14613
14641
|
return {
|
|
14614
14642
|
engine,
|
|
14615
14643
|
image: screenshot.toString("base64"),
|
|
14616
14644
|
width: metrics.w,
|
|
14617
|
-
height: metrics.h
|
|
14645
|
+
height: metrics.h,
|
|
14646
|
+
consoleErrors
|
|
14618
14647
|
};
|
|
14619
14648
|
}
|
|
14620
14649
|
async function render(code, framework, options = {}) {
|
|
14621
14650
|
const engines = options.engines ?? ["chromium"];
|
|
14622
|
-
const html = wrapComponent(
|
|
14651
|
+
const html = wrapComponent(
|
|
14652
|
+
code,
|
|
14653
|
+
framework,
|
|
14654
|
+
options.darkMode ?? false,
|
|
14655
|
+
options.css ?? ""
|
|
14656
|
+
);
|
|
14623
14657
|
const results = await Promise.all(
|
|
14624
14658
|
engines.map((e) => renderSingle(e, html, options))
|
|
14625
14659
|
);
|
|
@@ -14629,9 +14663,22 @@ async function screenshotUrl(url2, options = {}) {
|
|
|
14629
14663
|
const engines = options.engines ?? ["chromium"];
|
|
14630
14664
|
const results = await Promise.all(
|
|
14631
14665
|
engines.map(async (engine) => {
|
|
14632
|
-
const {
|
|
14666
|
+
const {
|
|
14667
|
+
width = 1280,
|
|
14668
|
+
height = 800,
|
|
14669
|
+
fullPage = true,
|
|
14670
|
+
waitFor = 0
|
|
14671
|
+
} = options;
|
|
14633
14672
|
const slot = await getSlot(engine);
|
|
14634
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));
|
|
14635
14682
|
const currentViewport = page.viewportSize();
|
|
14636
14683
|
if (currentViewport?.width !== width || currentViewport?.height !== height) {
|
|
14637
14684
|
await page.setViewportSize({ width, height });
|
|
@@ -14647,11 +14694,137 @@ async function screenshotUrl(url2, options = {}) {
|
|
|
14647
14694
|
w: document.documentElement.scrollWidth,
|
|
14648
14695
|
h: document.documentElement.scrollHeight
|
|
14649
14696
|
}));
|
|
14650
|
-
|
|
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
|
+
};
|
|
14651
14705
|
})
|
|
14652
14706
|
);
|
|
14653
14707
|
return results;
|
|
14654
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
|
+
}
|
|
14655
14828
|
async function shutdown() {
|
|
14656
14829
|
for (const slot of pool.values()) {
|
|
14657
14830
|
await slot.browser.close().catch(() => {
|
|
@@ -14661,6 +14834,18 @@ async function shutdown() {
|
|
|
14661
14834
|
}
|
|
14662
14835
|
|
|
14663
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();
|
|
14664
14849
|
var server = new McpServer({
|
|
14665
14850
|
name: "frameshot",
|
|
14666
14851
|
version: "0.1.0"
|
|
@@ -14670,20 +14855,47 @@ server.tool(
|
|
|
14670
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.",
|
|
14671
14856
|
{
|
|
14672
14857
|
code: external_exports.string().describe("Component code to render"),
|
|
14673
|
-
framework: external_exports.enum(["html", "react", "vue"]).default("react").describe("Framework: html, react, or
|
|
14674
|
-
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
|
+
),
|
|
14675
14862
|
width: external_exports.number().optional().default(1280).describe("Viewport width (px)"),
|
|
14676
14863
|
height: external_exports.number().optional().default(800).describe("Viewport height (px)"),
|
|
14677
|
-
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)")
|
|
14678
14870
|
},
|
|
14679
|
-
async ({
|
|
14871
|
+
async ({
|
|
14872
|
+
code,
|
|
14873
|
+
framework,
|
|
14874
|
+
engines,
|
|
14875
|
+
width,
|
|
14876
|
+
height,
|
|
14877
|
+
fullPage,
|
|
14878
|
+
darkMode,
|
|
14879
|
+
colorSchemes,
|
|
14880
|
+
css
|
|
14881
|
+
}) => {
|
|
14680
14882
|
try {
|
|
14681
|
-
const
|
|
14682
|
-
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
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();
|
|
14687
14899
|
const content = results.flatMap((r) => [
|
|
14688
14900
|
{
|
|
14689
14901
|
type: "image",
|
|
@@ -14692,13 +14904,18 @@ server.tool(
|
|
|
14692
14904
|
},
|
|
14693
14905
|
{
|
|
14694
14906
|
type: "text",
|
|
14695
|
-
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")}` : ""}`
|
|
14696
14910
|
}
|
|
14697
14911
|
]);
|
|
14698
14912
|
return { content };
|
|
14699
14913
|
} catch (error51) {
|
|
14700
14914
|
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14701
|
-
return {
|
|
14915
|
+
return {
|
|
14916
|
+
content: [{ type: "text", text: `Render failed: ${msg}` }],
|
|
14917
|
+
isError: true
|
|
14918
|
+
};
|
|
14702
14919
|
}
|
|
14703
14920
|
}
|
|
14704
14921
|
);
|
|
@@ -14728,19 +14945,276 @@ server.tool(
|
|
|
14728
14945
|
},
|
|
14729
14946
|
{
|
|
14730
14947
|
type: "text",
|
|
14731
|
-
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")}` : ""}`
|
|
14732
14951
|
}
|
|
14733
14952
|
]);
|
|
14734
14953
|
return { content };
|
|
14735
14954
|
} catch (error51) {
|
|
14736
14955
|
const msg = error51 instanceof Error ? error51.message : String(error51);
|
|
14737
|
-
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
|
+
};
|
|
14738
15214
|
}
|
|
14739
15215
|
}
|
|
14740
15216
|
);
|
|
14741
15217
|
var transport = new StdioServerTransport();
|
|
14742
|
-
warmup(["chromium"]).catch(() => {
|
|
14743
|
-
});
|
|
14744
15218
|
await server.connect(transport);
|
|
14745
15219
|
process.on("SIGINT", async () => {
|
|
14746
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
|
},
|