awwwards-mcp 1.2.0 → 1.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 +3 -3
- package/dist/capture.js +7 -1
- package/dist/cli.js +7 -0
- package/dist/motion.js +11 -2
- package/dist/server.js +10 -3
- package/dist/structure.js +7 -1
- package/dist/viewport.js +7 -0
- package/package.json +1 -1
- package/skills/awwwards-inspiration/SKILL.md +7 -3
- package/skills/awwwards-motion-study/SKILL.md +3 -0
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@ of any site: color palette, tech stack, design elements, award history.
|
|
|
18
18
|
| `get_site_details` | Full design DNA for one site: palette, technologies, elements, awards, description. |
|
|
19
19
|
| `get_site_elements` | Component-level visuals for one site: each element's poster image inline (3D models, video content, mobile layouts, microcopy…) + video URLs. |
|
|
20
20
|
| `list_categories` | Every filter the agent can search by (200+ tags, 27 colors). |
|
|
21
|
-
| `capture_live_site` | Optional: fresh full-page screenshot of any live URL. Waits for `load` + a settle window with a bounded pre-scroll, so heavy sites work (`waitStrategy: "networkidle"` available). (needs [playwright](https://playwright.dev)). |
|
|
22
|
-
| `analyze_page_structure` | Section band map of any page (live URL or local file:// build): tag, background, offset, height per band. Compare a reference site's structure against your build. Same heavy-site-friendly wait (`waitStrategy: "networkidle"` available). (needs [playwright](https://playwright.dev)). |
|
|
23
|
-
| `record_site_motion` | Optional: short motion-through video of a live URL — preloader, scroll-triggered and hover/cursor animations. Returns an inline filmstrip JPEG plus the saved .webm path. (needs [playwright](https://playwright.dev) + ffmpeg-static). |
|
|
21
|
+
| `capture_live_site` | Optional: fresh full-page screenshot of any live URL. Waits for `load` + a settle window with a bounded pre-scroll, so heavy sites work (`waitStrategy: "networkidle"` available). Pass `viewport: "mobile"` for the 390×844 iPhone-class render (`"desktop"` 1440×900 default). (needs [playwright](https://playwright.dev)). |
|
|
22
|
+
| `analyze_page_structure` | Section band map of any page (live URL or local file:// build): tag, background, offset, height per band. Compare a reference site's structure against your build. Same heavy-site-friendly wait (`waitStrategy: "networkidle"` available); `viewport: "mobile"` analyzes the phone-class layout (`"desktop"` default). (needs [playwright](https://playwright.dev)). |
|
|
23
|
+
| `record_site_motion` | Optional: short motion-through video of a live URL — preloader, scroll-triggered and hover/cursor animations. Returns an inline filmstrip JPEG plus the saved .webm path. `viewport: "mobile"` records at phone size — the filmstrip renders at the selected viewport, no pillarboxing (`"desktop"` default). (needs [playwright](https://playwright.dev) + ffmpeg-static). |
|
|
24
24
|
|
|
25
25
|
## Setup
|
|
26
26
|
|
package/dist/capture.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
// CAPTURE_INSTALL_HINT from here) — both sides only use the other's bindings
|
|
7
7
|
// at call time, which ESM resolves fine.
|
|
8
8
|
import { preScroll } from "./structure.js";
|
|
9
|
+
import { resolveViewport } from "./viewport.js";
|
|
9
10
|
export const CAPTURE_INSTALL_HINT = "Full-page capture needs Playwright, which is an optional dependency.\n" +
|
|
10
11
|
"Install it with: npm install -D playwright && npx playwright install chromium\n" +
|
|
11
12
|
"Then retry the capture or structure tool.";
|
|
@@ -29,7 +30,12 @@ loader = () => import("playwright"), opts) {
|
|
|
29
30
|
}
|
|
30
31
|
try {
|
|
31
32
|
const waitStrategy = opts?.waitStrategy ?? "load";
|
|
32
|
-
|
|
33
|
+
// Same viewport-profile split as analyzePageStructure (structure.ts):
|
|
34
|
+
// width/height fill the `viewport` key, mobile-profile flags spread in as
|
|
35
|
+
// sibling context options. Desktop resolves to no extra fields, so the
|
|
36
|
+
// default call shape is unchanged.
|
|
37
|
+
const { width, height, ...contextOpts } = resolveViewport(opts?.viewport);
|
|
38
|
+
const page = await browser.newPage({ viewport: { width, height }, ...contextOpts });
|
|
33
39
|
await page.goto(url, { waitUntil: waitStrategy, timeout: 45_000 });
|
|
34
40
|
// "load" can fire before late XHRs settle, so give the page a fixed
|
|
35
41
|
// settle window; networkidle already means the network went quiet.
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,10 @@ const waitStrategySchema = z
|
|
|
23
23
|
.enum(["load", "networkidle"])
|
|
24
24
|
.default("load")
|
|
25
25
|
.describe("'load' + settle works on heavy sites; 'networkidle' waits for total quiet");
|
|
26
|
+
const viewportSchema = z
|
|
27
|
+
.enum(["desktop", "mobile"])
|
|
28
|
+
.default("desktop")
|
|
29
|
+
.describe("desktop = 1440x900 (default); mobile = 390x844 iPhone-class with deviceScaleFactor 3, isMobile + hasTouch");
|
|
26
30
|
const cacheRoot = process.env.AWWWARDS_CACHE_DIR ?? join(homedir(), ".awwwards-mcp");
|
|
27
31
|
let cache;
|
|
28
32
|
try {
|
|
@@ -77,11 +81,13 @@ server.tool("list_categories", "List the filter taxonomy available on Awwwards:
|
|
|
77
81
|
server.tool("capture_live_site", "Take a fresh full-page screenshot of a live website URL using a headless browser. Requires the optional playwright dependency.", {
|
|
78
82
|
url: z.string().url().describe("Absolute URL of the site to capture"),
|
|
79
83
|
waitStrategy: waitStrategySchema,
|
|
84
|
+
viewport: viewportSchema,
|
|
80
85
|
}, (args) => asMcpResult(handlers.capture_live_site(args)));
|
|
81
86
|
server.tool("analyze_page_structure", "Extract a page's section band map (tag, label, background color, offset, height per band) via a headless browser. Works on live URLs and file:// paths — use it to compare a reference site's structure against your local build.", {
|
|
82
87
|
url: z.string().url().describe("Absolute URL (https:// or file://) of the page to analyze"),
|
|
83
88
|
maxBands: z.number().int().min(5).max(60).default(40).describe("Cap on returned bands"),
|
|
84
89
|
waitStrategy: waitStrategySchema,
|
|
90
|
+
viewport: viewportSchema,
|
|
85
91
|
}, (args) => asMcpResult(handlers.analyze_page_structure(args)));
|
|
86
92
|
server.tool("record_site_motion", "Record a short motion-through video of a live website — preloader, scroll-triggered and hover/cursor animations — and return an inline filmstrip JPEG plus the .webm path. Requires the optional playwright and ffmpeg-static dependencies.", {
|
|
87
93
|
url: z.string().url().describe("Absolute URL of the site to record"),
|
|
@@ -93,6 +99,7 @@ server.tool("record_site_motion", "Record a short motion-through video of a live
|
|
|
93
99
|
.default(16)
|
|
94
100
|
.describe("Filmstrip tile count (default 16 → a 4x4 grid)"),
|
|
95
101
|
waitStrategy: waitStrategySchema,
|
|
102
|
+
viewport: viewportSchema,
|
|
96
103
|
}, (args) => asMcpResult(handlers.record_site_motion(args)));
|
|
97
104
|
// Auto-refresh: if the index is stale (or absent) and no crawl is running,
|
|
98
105
|
// re-index in the background. Serving is never blocked; errors are stderr-only.
|
package/dist/motion.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, st
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { CAPTURE_INSTALL_HINT } from "./capture.js";
|
|
7
|
+
import { resolveViewport } from "./viewport.js";
|
|
7
8
|
export const MOTION_FFMPEG_HINT = "Motion recording needs ffmpeg-static, which is an optional dependency.\n" +
|
|
8
9
|
"Install it with: npm install -D ffmpeg-static\n" +
|
|
9
10
|
"Then retry record_site_motion.";
|
|
@@ -167,9 +168,17 @@ export async function recordSiteMotion(url, opts) {
|
|
|
167
168
|
const videoTmp = mkdtempSync(join(opts.cacheImagesDir, ".video-tmp-"));
|
|
168
169
|
const waitStrategy = opts.waitStrategy ?? "load";
|
|
169
170
|
try {
|
|
171
|
+
// Viewport profile split (same as structure/capture): width/height fill
|
|
172
|
+
// the `viewport` key; the mobile-profile flags (deviceScaleFactor/
|
|
173
|
+
// isMobile/hasTouch) spread in as sibling context options AFTER the
|
|
174
|
+
// existing fields. recordVideo.size derives from the same profile so the
|
|
175
|
+
// video canvas matches the viewport (desktop keeps the 1440x900 canvas; a
|
|
176
|
+
// mobile recording gets a 390x844 canvas instead of a pillarboxed one).
|
|
177
|
+
const { width, height, ...contextOpts } = resolveViewport(opts.viewport);
|
|
170
178
|
const context = await browser.newContext({
|
|
171
|
-
viewport: { width
|
|
172
|
-
recordVideo: { dir: videoTmp, size: { width
|
|
179
|
+
viewport: { width, height },
|
|
180
|
+
recordVideo: { dir: videoTmp, size: { width, height } },
|
|
181
|
+
...contextOpts,
|
|
173
182
|
});
|
|
174
183
|
try {
|
|
175
184
|
const page = await context.newPage();
|
package/dist/server.js
CHANGED
|
@@ -419,7 +419,12 @@ export function createHandlers(deps) {
|
|
|
419
419
|
// the injectable playwright loader — opts must land in fourth place.
|
|
420
420
|
const capture = deps.captureFn ??
|
|
421
421
|
((url, imagesDir, opts) => import("./capture.js").then((m) => m.captureLiveSite(url, imagesDir, undefined, opts)));
|
|
422
|
-
|
|
422
|
+
// The tool schema defaults viewport to "desktop" (zod); the ?? keeps
|
|
423
|
+
// direct handler calls on the same explicit path.
|
|
424
|
+
const result = await capture(args.url, cache.imagesDir, {
|
|
425
|
+
waitStrategy: args.waitStrategy,
|
|
426
|
+
viewport: args.viewport ?? "desktop",
|
|
427
|
+
});
|
|
423
428
|
if ("error" in result)
|
|
424
429
|
return { content: [text(result.error)], isError: true };
|
|
425
430
|
return {
|
|
@@ -442,6 +447,7 @@ export function createHandlers(deps) {
|
|
|
442
447
|
((url, maxBands, opts) => import("./structure.js").then((m) => m.analyzePageStructure(url, undefined, maxBands, opts)));
|
|
443
448
|
const structure = await analyze(args.url, args.maxBands, {
|
|
444
449
|
waitStrategy: args.waitStrategy,
|
|
450
|
+
viewport: args.viewport ?? "desktop",
|
|
445
451
|
});
|
|
446
452
|
if ("error" in structure)
|
|
447
453
|
return { content: [text(structure.error)], isError: true };
|
|
@@ -454,14 +460,15 @@ export function createHandlers(deps) {
|
|
|
454
460
|
async function record_site_motion(args) {
|
|
455
461
|
try {
|
|
456
462
|
// Lazy default: playwright/ffmpeg are only touched when the tool runs.
|
|
457
|
-
// The default forwards motionOpts wholesale, so waitStrategy
|
|
458
|
-
// recordSiteMotion's MotionOpts (which already accepts
|
|
463
|
+
// The default forwards motionOpts wholesale, so waitStrategy and viewport
|
|
464
|
+
// flow into recordSiteMotion's MotionOpts (which already accepts both).
|
|
459
465
|
const motion = deps.motionFn ??
|
|
460
466
|
((url, motionOpts) => import("./motion.js").then((m) => m.recordSiteMotion(url, motionOpts)));
|
|
461
467
|
const result = await motion(args.url, {
|
|
462
468
|
cacheImagesDir: cache.imagesDir,
|
|
463
469
|
frames: args.frames,
|
|
464
470
|
waitStrategy: args.waitStrategy,
|
|
471
|
+
viewport: args.viewport ?? "desktop",
|
|
465
472
|
});
|
|
466
473
|
if ("error" in result)
|
|
467
474
|
return { content: [text(result.error)], isError: true };
|
package/dist/structure.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CAPTURE_INSTALL_HINT } from "./capture.js";
|
|
2
|
+
import { resolveViewport } from "./viewport.js";
|
|
2
3
|
// Runs IN THE PAGE via page.evaluate. Collects full-width, tall, opaque
|
|
3
4
|
// elements as band candidates; body is always the base candidate. Gradient
|
|
4
5
|
// shorthand backgrounds leave backgroundColor transparent — such sections
|
|
@@ -182,7 +183,12 @@ export async function analyzePageStructure(url, loader = () => import("playwrigh
|
|
|
182
183
|
}
|
|
183
184
|
try {
|
|
184
185
|
const waitStrategy = opts?.waitStrategy ?? "load";
|
|
185
|
-
|
|
186
|
+
// Viewport profile split: width/height fill playwright's `viewport` key;
|
|
187
|
+
// the mobile-profile flags (deviceScaleFactor/isMobile/hasTouch) are
|
|
188
|
+
// sibling context options. The desktop profile resolves to no extra
|
|
189
|
+
// fields, so the default call shape is unchanged.
|
|
190
|
+
const { width, height, ...contextOpts } = resolveViewport(opts?.viewport);
|
|
191
|
+
const page = await browser.newPage({ viewport: { width, height }, ...contextOpts });
|
|
186
192
|
await page.goto(url, { waitUntil: waitStrategy, timeout: 45_000 });
|
|
187
193
|
// "load" can fire before late XHRs settle, so give the page a fixed
|
|
188
194
|
// settle window; networkidle already means the network went quiet.
|
package/dist/viewport.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "awwwards-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Free MCP server giving AI agents design inspiration from Awwwards: search award-winning sites with inline screenshots and extract design DNA.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,9 @@ Run this loop before building anything visual:
|
|
|
38
38
|
full-page PNG — every section, top to bottom. Cached Awwwards screenshots
|
|
39
39
|
are hero-only crops (~880×660) and hide everything below the fold: the
|
|
40
40
|
sections that make a site's structure distinctive (pricing, feature
|
|
41
|
-
layouts, contrast breaks, footer) were never visible in them.
|
|
41
|
+
layouts, contrast breaks, footer) were never visible in them. For
|
|
42
|
+
mobile-excellence references pass `viewport: "mobile"` — and capture BOTH
|
|
43
|
+
viewports when the desktop and mobile designs diverge.
|
|
42
44
|
5. **Get the design DNA.** Call `get_site_details` on the top pick for its
|
|
43
45
|
palette, technologies, design elements, awards, and description. If it
|
|
44
46
|
reports a layout-drift error, fall back to judging the shortlisted
|
|
@@ -68,8 +70,10 @@ Run this loop before building anything visual:
|
|
|
68
70
|
8. **Verify structure, then polish.** After building, capture your own build
|
|
69
71
|
full-page (`capture_live_site` on its `file://` or served URL) and run
|
|
70
72
|
`analyze_page_structure` on BOTH the reference and the build. Compare band
|
|
71
|
-
maps section by section (count, order, backgrounds, heights).
|
|
72
|
-
|
|
73
|
+
maps section by section (count, order, backgrounds, heights). Match the
|
|
74
|
+
reference's viewport when comparing: for mobile-excellence references pass
|
|
75
|
+
`viewport: "mobile"`, and capture BOTH viewports when the design diverges.
|
|
76
|
+
Fix distribution mismatches first — a section that is 3× the reference's height
|
|
73
77
|
is a structural bug no amount of pixel polish fixes. Match the reference's
|
|
74
78
|
band structure, never just its total height.
|
|
75
79
|
|
|
@@ -68,6 +68,9 @@ Capture rules that prevent re-shoots:
|
|
|
68
68
|
- **Capture BEFORE building** — the doctrine step. Review first, code second.
|
|
69
69
|
- Use a **consistent viewport** (1440×900 matches the QA scripts) so your
|
|
70
70
|
reference tiles and build tiles are comparable.
|
|
71
|
+
- For phone-class references pass `record_site_motion` a `viewport: "mobile"`
|
|
72
|
+
(390×844 @3x with isMobile + hasTouch) — and capture BOTH viewports when
|
|
73
|
+
the desktop and mobile designs diverge.
|
|
71
74
|
- **Pre-scroll** to fire lazy content, scroll back to top, then record —
|
|
72
75
|
otherwise reveal-on-scroll sections record as blank boxes.
|
|
73
76
|
- For multi-page sites, record **each page** you'll rebuild (home, projects,
|