awwwards-mcp 1.7.0 → 1.7.2
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/dist/capture.js +2 -2
- package/dist/cli.js +5 -0
- package/dist/env-check.js +108 -0
- package/dist/motion.js +1 -1
- package/dist/server.js +3 -2
- package/package.json +3 -2
package/dist/capture.js
CHANGED
|
@@ -8,8 +8,8 @@ import { join } from "node:path";
|
|
|
8
8
|
import { preScroll } from "./structure.js";
|
|
9
9
|
import { resolveViewport } from "./viewport.js";
|
|
10
10
|
export const CAPTURE_INSTALL_HINT = "Full-page capture needs Playwright, which is an optional dependency.\n" +
|
|
11
|
-
"Install it
|
|
12
|
-
"Then retry the capture or structure tool.";
|
|
11
|
+
"Install it where this server runs (globally works for a global install): npm install -g playwright && npx playwright install chromium\n" +
|
|
12
|
+
"Then retry the capture or structure tool (or restart your agent afterward).";
|
|
13
13
|
export async function captureLiveSite(url, imagesDir,
|
|
14
14
|
// "as string" keeps Playwright an unresolved optional dependency at compile time;
|
|
15
15
|
// Node resolves it (and may throw) at runtime, which the try/catch below handles.
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createHandlers } from "./server.js";
|
|
|
13
13
|
import { captureLiveSite } from "./capture.js";
|
|
14
14
|
import { runIndexer, shouldAutoIndex } from "./indexer.js";
|
|
15
15
|
import { checkForUpdate } from "./version-check.js";
|
|
16
|
+
import { checkEnvironment } from "./env-check.js";
|
|
16
17
|
// The handlers return ToolResponse, which is structurally identical to the
|
|
17
18
|
// SDK's CallToolResult at runtime ({ content, isError? }). CallToolResult's
|
|
18
19
|
// schema additionally carries a [k: string]: unknown index signature that a
|
|
@@ -155,4 +156,8 @@ if (shouldAutoIndex(cache)) {
|
|
|
155
156
|
// Update notice: once a day, compare against the npm registry; stderr-only,
|
|
156
157
|
// never blocks serving. AWWWARDS_AUTO_UPDATE=1 opts into background install.
|
|
157
158
|
checkForUpdate(pkgJson.version);
|
|
159
|
+
// Environment notice: once a day, verify playwright/ffmpeg are usable from
|
|
160
|
+
// the server's own install (a real chromium launch, not just an import);
|
|
161
|
+
// stderr-only, never blocks serving.
|
|
162
|
+
checkEnvironment();
|
|
158
163
|
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Environment probe: at startup (once per day), report whether the optional
|
|
2
|
+
// heavy deps the live-capture and motion tools need are actually usable, so
|
|
3
|
+
// the agent learns about a missing playwright/ffmpeg BEFORE a capture fails
|
|
4
|
+
// mid-task instead of after. Companion to version-check.ts; same constraints
|
|
5
|
+
// of a stdio MCP server:
|
|
6
|
+
// - stdout is the JSON-RPC channel → notices go to stderr only
|
|
7
|
+
// - serving must never wait on this → fire-and-forget, per-step timeouts,
|
|
8
|
+
// every failure swallowed (missing module, crashed spawn, dead FS)
|
|
9
|
+
// - at most one probe per day per machine (state in the cache dir)
|
|
10
|
+
// A real chromium launch is mandatory — import("playwright") succeeding does
|
|
11
|
+
// NOT mean the browser binary is present (the browsers are a separate
|
|
12
|
+
// download into the user's cache; the exact failure class this catches).
|
|
13
|
+
//
|
|
14
|
+
// cwd note: the probe spawns `node -e` from THIS package's root, because
|
|
15
|
+
// `node -e` resolves modules from cwd — walking up from a random MCP-client
|
|
16
|
+
// cwd would miss a globally-installed playwright/ffmpeg, which the package
|
|
17
|
+
// itself resolves fine from dist/../node_modules up to the global root.
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
// dist/env-check.js → one level up is the package root (the repo root in dev).
|
|
25
|
+
const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
26
|
+
export function envStateFile() {
|
|
27
|
+
const cacheRoot = process.env.AWWWARDS_CACHE_DIR ?? join(homedir(), ".awwwards-mcp");
|
|
28
|
+
return join(cacheRoot, "env-check.json");
|
|
29
|
+
}
|
|
30
|
+
function readState() {
|
|
31
|
+
try {
|
|
32
|
+
if (!existsSync(envStateFile()))
|
|
33
|
+
return null;
|
|
34
|
+
return JSON.parse(readFileSync(envStateFile(), "utf8"));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function writeState() {
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(dirname(envStateFile()), { recursive: true });
|
|
43
|
+
writeFileSync(envStateFile(), JSON.stringify({ checkedAt: Date.now() }));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* state is an optimization; unwritable state just means we probe again */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const ENV_FIX_HINTS = {
|
|
50
|
+
playwright: "awwwards-mcp: live capture/analyze/motion tools need Playwright — run: npm install -g playwright && npx playwright install chromium (restart your agent afterward)\n",
|
|
51
|
+
ffmpeg: "awwwards-mcp: motion recording needs ffmpeg-static — run: npm install -g ffmpeg-static (restart your agent afterward)\n",
|
|
52
|
+
};
|
|
53
|
+
function run(cmd, args, timeoutMs) {
|
|
54
|
+
return new Promise((res) => {
|
|
55
|
+
try {
|
|
56
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd: pkgRoot });
|
|
57
|
+
let out = "";
|
|
58
|
+
const timer = setTimeout(() => child.kill(), timeoutMs);
|
|
59
|
+
child.stdout.on("data", (d) => (out += String(d)));
|
|
60
|
+
child.stderr.on("data", (d) => (out += String(d)));
|
|
61
|
+
child.on("close", (code) => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
res({ code, out: out.slice(0, 300) });
|
|
64
|
+
});
|
|
65
|
+
child.on("error", () => {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
res({ code: null, out: "" });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
res({ code: null, out: "" });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const LAUNCH_SCRIPT = 'import("playwright").then(m=>m.chromium.launch()).then(b=>b.close()).then(()=>console.log("ok"))' +
|
|
76
|
+
'.catch(e=>{console.error(e.message);process.exit(1)})';
|
|
77
|
+
const FFMPEG_SCRIPT = 'import("ffmpeg-static").then(m=>console.log(m.default??m)).catch(()=>process.exit(1))';
|
|
78
|
+
async function defaultProbe() {
|
|
79
|
+
const pw = await run(process.execPath, ["-e", LAUNCH_SCRIPT], 30_000);
|
|
80
|
+
if (pw.code !== 0)
|
|
81
|
+
console.error(ENV_FIX_HINTS.playwright + (pw.out ? ` last error: ${pw.out}` : ""));
|
|
82
|
+
const ff = await run(process.execPath, ["-e", FFMPEG_SCRIPT], 10_000);
|
|
83
|
+
if (ff.code !== 0)
|
|
84
|
+
console.error(ENV_FIX_HINTS.ffmpeg);
|
|
85
|
+
// A flaky chromium launch (first-run policy, AV scan) must not crash the
|
|
86
|
+
// probe — it just counts as a failure, so tomorrow still re-checks.
|
|
87
|
+
return pw.code === 0 && ff.code === 0;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Once a day, probe playwright + ffmpeg from the server's own install
|
|
91
|
+
* location. Fire-and-forget; stdout untouched; every failure swallowed.
|
|
92
|
+
* `probe` is injectable for tests; resolves with true on a healthy env,
|
|
93
|
+
* false/throw when unusable — the scheduler records state on true only.
|
|
94
|
+
*/
|
|
95
|
+
export function checkEnvironment(opts) {
|
|
96
|
+
const state = readState();
|
|
97
|
+
if (state && Date.now() - state.checkedAt < CHECK_INTERVAL_MS)
|
|
98
|
+
return;
|
|
99
|
+
// Fire-and-forget AND swallow-everything: a rejected probe must not surface
|
|
100
|
+
// as an unhandled rejection or break server startup.
|
|
101
|
+
const probe = opts?.probe ?? defaultProbe;
|
|
102
|
+
void probe()
|
|
103
|
+
.then((healthy) => {
|
|
104
|
+
if (healthy === true)
|
|
105
|
+
writeState();
|
|
106
|
+
})
|
|
107
|
+
.catch(() => { });
|
|
108
|
+
}
|
package/dist/motion.js
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { CAPTURE_INSTALL_HINT } from "./capture.js";
|
|
7
7
|
import { resolveViewport } from "./viewport.js";
|
|
8
8
|
export const MOTION_FFMPEG_HINT = "Motion recording needs ffmpeg-static, which is an optional dependency.\n" +
|
|
9
|
-
"Install it
|
|
9
|
+
"Install it where this server runs (globally works for a global install): npm install -g ffmpeg-static\n" +
|
|
10
10
|
"Then retry record_site_motion.";
|
|
11
11
|
// Recording-pass timings, tuned against the 30s MCP tool ceiling. The
|
|
12
12
|
// validated script (scripts/record-scrollthrough.mjs) spends well past 45s
|
package/dist/server.js
CHANGED
|
@@ -83,6 +83,7 @@ function errorResponse(err) {
|
|
|
83
83
|
export const MOTION_DNA_TTL_MS = 90 * 24 * 60 * 60 * 1000;
|
|
84
84
|
export function createHandlers(deps) {
|
|
85
85
|
const { client, cache } = deps;
|
|
86
|
+
const nowMs = deps.now ?? Date.now;
|
|
86
87
|
// Which filter wins the URL (combined filter URLs 404 on awwwards.com).
|
|
87
88
|
const urlSource = (f) => f.color ? "color" : f.award ? "award" : f.technology ? "technology" : f.tags?.length ? "tag" : "none";
|
|
88
89
|
// Two modes:
|
|
@@ -793,7 +794,7 @@ export function createHandlers(deps) {
|
|
|
793
794
|
// ---- longitudinal monitoring ----
|
|
794
795
|
async function new_winners(args) {
|
|
795
796
|
try {
|
|
796
|
-
const res = await fetchNewWinners(client, cache, { award: args.award ?? "sotd" });
|
|
797
|
+
const res = await fetchNewWinners(client, cache, { award: args.award ?? "sotd", now: nowMs });
|
|
797
798
|
// Seed the sites cache too — next search_sites serves these rows
|
|
798
799
|
// without re-fetching the same listing.
|
|
799
800
|
if (res.newWinners.length > 0)
|
|
@@ -808,7 +809,7 @@ export function createHandlers(deps) {
|
|
|
808
809
|
for (const site of res.newWinners) {
|
|
809
810
|
try {
|
|
810
811
|
const html = await client.getHtml(site.detailPath);
|
|
811
|
-
records.push(...recordsFromSiteHtml(html, site, Math.floor(
|
|
812
|
+
records.push(...recordsFromSiteHtml(html, site, Math.floor(nowMs() / 1000)));
|
|
812
813
|
backfilled++;
|
|
813
814
|
}
|
|
814
815
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "awwwards-mcp",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
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",
|
|
@@ -50,10 +50,11 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^24.0.0",
|
|
53
|
+
"@vitest/mocker": "^4.1.11",
|
|
53
54
|
"ffmpeg-static": "^5.3.0",
|
|
54
55
|
"playwright": "^1.63.0",
|
|
55
56
|
"tsx": "^4.19.0",
|
|
56
57
|
"typescript": "^5.6.0",
|
|
57
|
-
"vitest": "^
|
|
58
|
+
"vitest": "^4.1.11"
|
|
58
59
|
}
|
|
59
60
|
}
|