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 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
- const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
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: 1440, height: 900 },
172
- recordVideo: { dir: videoTmp, size: { width: 1440, height: 900 } },
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
- const result = await capture(args.url, cache.imagesDir, { waitStrategy: args.waitStrategy });
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 flows into
458
- // recordSiteMotion's MotionOpts (which already accepts it).
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
- const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
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.
@@ -0,0 +1,7 @@
1
+ export const VIEWPORT_PROFILES = {
2
+ desktop: { width: 1440, height: 900 },
3
+ mobile: { width: 390, height: 844, deviceScaleFactor: 3, isMobile: true, hasTouch: true },
4
+ };
5
+ export function resolveViewport(name) {
6
+ return VIEWPORT_PROFILES[name ?? "desktop"];
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "awwwards-mcp",
3
- "version": "1.2.0",
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). Fix
72
- distribution mismatches first — a section that is 3× the reference's height
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,