webcake-landing-mcp 1.5.11 → 1.6.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/dist/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.6.0",
|
|
4
|
+
"d": "14/10/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "The self-hosted screenshot route behind render_preview now caps concurrent Playwright captures at RENDER_SCREENSHOT_CONCURRENCY (default 2), queuing…",
|
|
7
|
+
"vi": "Route screenshot self-hosted phía sau render_preview nay giới hạn số lượt chụp Playwright chạy đồng thời theo RENDER_SCREENSHOT_CONCURRENCY (mặc…"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.5.11",
|
|
4
11
|
"d": "13/10/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Changed",
|
|
34
41
|
"en": "The ingest_url and render_preview tool descriptions now link to the API docs (docs/tools.md for both, plus the Microlink API reference for…",
|
|
35
42
|
"vi": "Mô tả của tool ingest_url và render_preview nay có thêm link tới tài liệu API (docs/tools.md cho cả hai, cùng với link tham chiếu API của Microlink…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.5.6",
|
|
39
|
-
"d": "13/10/2026",
|
|
40
|
-
"type": "Fixed",
|
|
41
|
-
"en": "The HTTP server now serves a 192px PNG at /favicon.ico, /favicon.png, and /apple-touch-icon.png (with matching <link rel=\"icon\">/<link…",
|
|
42
|
-
"vi": "HTTP server nay phục vụ PNG 192px tại /favicon.ico, /favicon.png, và /apple-touch-icon.png (kèm thẻ <link rel=\"icon\">/<link rel=\"apple-touch-icon\">…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -99,6 +99,62 @@ async function withContext(width, scale, fn) {
|
|
|
99
99
|
error: "playwright is not installed on this host (run: npm i playwright && npx playwright install chromium)",
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
await acquireSlot();
|
|
103
|
+
try {
|
|
104
|
+
return await withContextUnlimited(width, scale, fn);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
releaseSlot();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// RAM guard for small VPSes: cap concurrent captures (each context is
|
|
112
|
+
// ~100–300 MB) and close the browser after an idle spell so it doesn't sit on
|
|
113
|
+
// memory / slowly leak. RENDER_SCREENSHOT_CONCURRENCY (default 2),
|
|
114
|
+
// RENDER_SCREENSHOT_IDLE_MS (default 5 min, 0 = keep the browser forever).
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
const MAX_CONCURRENT = Math.max(1, parseInt(process.env.RENDER_SCREENSHOT_CONCURRENCY ?? "", 10) || 2);
|
|
117
|
+
const idleEnv = parseInt(process.env.RENDER_SCREENSHOT_IDLE_MS ?? "", 10);
|
|
118
|
+
const IDLE_CLOSE_MS = Number.isFinite(idleEnv) && idleEnv >= 0 ? idleEnv : 300_000;
|
|
119
|
+
let active = 0;
|
|
120
|
+
const waiters = [];
|
|
121
|
+
let idleTimer = null;
|
|
122
|
+
/** Wait for a capture slot. ponytail: unbounded FIFO queue — the HTTP caller's own timeout bounds the wait. */
|
|
123
|
+
export async function acquireSlot() {
|
|
124
|
+
if (idleTimer) {
|
|
125
|
+
clearTimeout(idleTimer);
|
|
126
|
+
idleTimer = null;
|
|
127
|
+
}
|
|
128
|
+
if (active < MAX_CONCURRENT) {
|
|
129
|
+
active++;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await new Promise((r) => waiters.push(r)); // releaseSlot hands its slot over (active unchanged)
|
|
133
|
+
}
|
|
134
|
+
/** Free a slot: hand it to the next waiter, else arm the idle close once nothing is running. */
|
|
135
|
+
export function releaseSlot() {
|
|
136
|
+
const next = waiters.shift();
|
|
137
|
+
if (next)
|
|
138
|
+
return next();
|
|
139
|
+
active--;
|
|
140
|
+
if (active === 0 && IDLE_CLOSE_MS > 0) {
|
|
141
|
+
idleTimer = setTimeout(closeIdleBrowser, IDLE_CLOSE_MS);
|
|
142
|
+
idleTimer.unref?.();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Snapshot of the limiter (smoke coverage). */
|
|
146
|
+
export function slotStats() {
|
|
147
|
+
return { active, waiting: waiters.length, max: MAX_CONCURRENT };
|
|
148
|
+
}
|
|
149
|
+
async function closeIdleBrowser() {
|
|
150
|
+
idleTimer = null;
|
|
151
|
+
const p = browserPromise;
|
|
152
|
+
browserPromise = null; // the next capture relaunches
|
|
153
|
+
const browser = p ? await p.catch(() => null) : null;
|
|
154
|
+
if (browser)
|
|
155
|
+
await browser.close().catch(() => { });
|
|
156
|
+
}
|
|
157
|
+
async function withContextUnlimited(width, scale, fn) {
|
|
102
158
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
103
159
|
let browser = await getBrowser();
|
|
104
160
|
if (!browser)
|
package/dist/smoke.js
CHANGED
|
@@ -21,7 +21,7 @@ import { buildConnectUrl, parseCallback } from "./auth/login.js";
|
|
|
21
21
|
import { redirectMatches } from "./auth/oauth-server.js";
|
|
22
22
|
import { isLocalPath, resolveLocalPath, sniffMime, localContentType } from "./tools/media.js";
|
|
23
23
|
import { buildMicrolinkUrl } from "./persistence/screenshot-client.js";
|
|
24
|
-
import { isPrivateHost, isAllowedScreenshotUrl } from "./persistence/screenshot-playwright.js";
|
|
24
|
+
import { isPrivateHost, isAllowedScreenshotUrl, acquireSlot, releaseSlot, slotStats } from "./persistence/screenshot-playwright.js";
|
|
25
25
|
import { iconifyCandidates } from "./persistence/icon-client.js";
|
|
26
26
|
import { collectExternalImageUrls, rewriteImageUrls, isRehostableImageUrl, isInlineBase64Image, parseInlineBase64Image } from "./persistence/rehost.js";
|
|
27
27
|
import { key as rehostCacheKey } from "./persistence/rehost-cache.js";
|
|
@@ -2265,6 +2265,22 @@ console.log("== upload_images: localContentType (ext + magic, pure offline) ==")
|
|
|
2265
2265
|
}
|
|
2266
2266
|
// == /api/render/screenshot: SSRF url guard (pure offline) ==
|
|
2267
2267
|
{
|
|
2268
|
+
console.log("\n== render screenshot: concurrency cap (pure offline) ==");
|
|
2269
|
+
{
|
|
2270
|
+
const max = slotStats().max;
|
|
2271
|
+
for (let i = 0; i < max; i++)
|
|
2272
|
+
await acquireSlot();
|
|
2273
|
+
let queuedGotSlot = false;
|
|
2274
|
+
const queued = acquireSlot().then(() => (queuedGotSlot = true));
|
|
2275
|
+
await Promise.resolve();
|
|
2276
|
+
check("slots: capped at max, extra caller waits", slotStats().active === max && slotStats().waiting === 1 && !queuedGotSlot);
|
|
2277
|
+
releaseSlot(); // hands the slot to the waiter
|
|
2278
|
+
await queued;
|
|
2279
|
+
check("slots: release hands slot to waiter", queuedGotSlot && slotStats().active === max && slotStats().waiting === 0);
|
|
2280
|
+
for (let i = 0; i < max; i++)
|
|
2281
|
+
releaseSlot();
|
|
2282
|
+
check("slots: all released", slotStats().active === 0);
|
|
2283
|
+
}
|
|
2268
2284
|
console.log("\n== render screenshot route: SSRF url guard (pure offline) ==");
|
|
2269
2285
|
// private / loopback / link-local hosts are blocked
|
|
2270
2286
|
check("isPrivateHost: localhost", isPrivateHost("localhost"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "MCP server exposing Webcake landing-page element schemas + AI usage hints, and persisting LLM-generated page sources to a Webcake backend.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/webcake-landing-mcp",
|
|
6
6
|
"type": "module",
|