webcake-landing-mcp 1.5.8 → 1.5.10
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 +14 -14
- package/dist/domains/landing/guide.js +12 -1
- package/dist/http.js +5 -0
- package/dist/persistence/html-ingest.js +27 -8
- package/dist/smoke.js +3 -1
- package/dist/tools/generation.js +4 -4
- package/dist/tools/ingest.js +7 -7
- package/dist/tools/media.js +8 -8
- package/dist/tools/persistence.js +17 -17
- package/dist/tools/reference.js +3 -3
- package/package.json +1 -1
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.5.10",
|
|
4
|
+
"d": "13/10/2026",
|
|
5
|
+
"type": "Changed",
|
|
6
|
+
"en": "Rewrote every tool and parameter description (new_element, new_page_skeleton, validate_page, layout, ingest_html, ingest_url, search_images,…",
|
|
7
|
+
"vi": "Viết lại toàn bộ mô tả tool và tham số (new_element, new_page_skeleton, validate_page, layout, ingest_html, ingest_url, search_images, get_icon_svg,…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.5.9",
|
|
11
|
+
"d": "13/10/2026",
|
|
12
|
+
"type": "Fixed",
|
|
13
|
+
"en": "The HTTP server now answers a request carrying an unknown mcp-session-id with 404 instead of 400, so clients re-initialize instead of staying stuck…",
|
|
14
|
+
"vi": "HTTP server nay trả về 404 (thay vì 400) khi nhận request với mcp-session-id không xác định, giúp client tự re-initialize thay vì bị kẹt ở trạng…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.5.8",
|
|
4
18
|
"d": "13/10/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Fixed",
|
|
27
41
|
"en": "The page/source/sections/patches parameters on validate_page, create_page, update_page, add_section, and patch_page now declare a typed JSON Schema…",
|
|
28
42
|
"vi": "Các tham số page/source/sections/patches của validate_page, create_page, update_page, add_section, và patch_page nay khai báo JSON Schema có kiểu rõ…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.5.4",
|
|
32
|
-
"d": "09/10/2026",
|
|
33
|
-
"type": "Added",
|
|
34
|
-
"en": "ingest_html and ingest_url now return font_groups (the page's self-hosted @font-face fonts, already shaped for settings.fontGroups) and…",
|
|
35
|
-
"vi": "ingest_html và ingest_url nay trả về font_groups (các font @font-face tự host của trang, đã đóng gói theo đúng dạng settings.fontGroups) và…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.5.3",
|
|
39
|
-
"d": "17/09/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "validate_page now detects inline base64 images (data:image/…;base64,… URIs) anywhere in an element's specials, a styles background, gallery…",
|
|
42
|
-
"vi": "validate_page nay phát hiện ảnh base64 nhúng (URI data:image/…;base64,…) ở bất kỳ đâu trong specials của phần tử, nền styles, gallery item.link,…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -168,4 +168,15 @@ REFERENCE INPUT (HTML page to clone or adapt)
|
|
|
168
168
|
- Map AST section roles to Webcake elements: hero → section (background image/overlay) + text-block H1 + text-block subheading + button; features → group per card (icon rectangle + text-block title + text-block body); stats bar → group with text-block per stat; pricing → group with text-block list + button; footer → section (dark bg) + text-block + links. When the ingested page contains a composite widget (phone/device mockup, chat thread, mini dashboard, browser frame) → rebuild it as ONE html-box, NOT as element soup. The full AST hands you the SOURCE for this: section.widgets = [{ hint, html, css? }] — the widget's cleaned outerHTML plus the stylesheet rules that style it. Build the html-box FROM that html verbatim: inline each css rule into the matching elements' style="" attributes (an html-box has no <style> scope), wrap in a root div with width:100%;height:100%;box-sizing:border-box;overflow:hidden, and size the html-box to the widget's box — do NOT re-imagine the widget's markup from the summary fields.
|
|
169
169
|
- Reference images are the user's assets — for BOTH intents (adapt AND clone), carry every real image URL found in the AST (images, background_images, og_image, canvas src/background) straight into the matching slot; the save auto-hosts it to the Webcake CDN (no upload_images needed, no hotlinking), so never replace it with a search_images stock photo or a placeholder. intent='adapt' rewrites TEXT for the user's brand, not the imagery; search_images only fills slots that have no source image.
|
|
170
170
|
- FONTS + TYPE SCALE (clone/adapt): the AST's \`font_groups\` are the page's SELF-HOSTED fonts (from its @font-face rules — the platform doesn't have them). Put them into settings.fontGroups VERBATIM and set each text element's styles.fontFamily to "'<group name>', sans-serif" (fontGeneral too when it's the body font); keep the original file URLs — every save downloads them, uploads them to the media collection and files the group into the org's Font Groups (the result's \`fonts\` report), so never skip or substitute the font. Families NOT in font_groups are Google fonts and load by name. Each section's \`text_styles\` {heading, body, cta} and the page-level \`typography\` are the source's COMPUTED type (cascade, @media, var(), clamp(), Tailwind classes): set desktop styles.fontSize = font_size, mobile styles.fontSize = mobile_font_size (else font_size, shrunk only if the mobile text math needs it), and fontWeight/fontFamily/lineHeight/letterSpacing EXACTLY — never eyeball a size the AST already gives. Any fontWeight works (500/600/800 included): the save adds the Google weight links the build would otherwise skip. An absolute-canvas \`source\` already carries all of this. For ingest_html of HTML copied from a live site, pass base_url so external stylesheets (and their fonts) are read. Custom font files belong to the source site's license — clone fonts only for pages the user owns or is allowed to copy; for a third-party ADAPT, prefer the nearest Google family over its font_groups.
|
|
171
|
-
- VERIFY THE CLONE VISUALLY (clones only — where fidelity is the whole point): for a clone/adapt build, do the VISUAL CHECK from BUILDING step 7 ONCE — screenshot the rendered preview with your own browser/screenshot capability (or render_preview as fallback), compare it side-by-side with the reference (screenshot the reference URL too if you only have HTML), patch_page the clear mismatches, and re-check ONLY if you changed something material (don't loop reflexively). For a NON-clone page, validate_page + the save-time auto-fix are the gate — skip the screenshot unless the user asks or you have a specific aesthetic doubt. Eyeballing a reference and rebuilding once typically lands ~60% similar; a targeted look-and-patch pass closes the gap when fidelity matters
|
|
171
|
+
- VERIFY THE CLONE VISUALLY (clones only — where fidelity is the whole point): for a clone/adapt build, do the VISUAL CHECK from BUILDING step 7 ONCE — screenshot the rendered preview with your own browser/screenshot capability (or render_preview as fallback), compare it side-by-side with the reference (screenshot the reference URL too if you only have HTML), patch_page the clear mismatches, and re-check ONLY if you changed something material (don't loop reflexively). For a NON-clone page, validate_page + the save-time auto-fix are the gate — skip the screenshot unless the user asks or you have a specific aesthetic doubt. Eyeballing a reference and rebuilding once typically lands ~60% similar; a targeted look-and-patch pass closes the gap when fidelity matters.
|
|
172
|
+
|
|
173
|
+
TOOL NOTES (how to sequence the tools — kept here rather than in the tool descriptions):
|
|
174
|
+
- Coordinates: call layout for centered boxes, rows, grids and stacks instead of hand-computing left/top; drop the returned boxes into responsive.<bp>.styles in the order you passed them.
|
|
175
|
+
- Elements: author nodes in the sparse shape new_element / get_element return; fetch several types at once with get_element({ types:[...] }).
|
|
176
|
+
- Validation: validate_page before every save and treat warnings as a fix list — fix and re-validate until only demonstrable false positives remain.
|
|
177
|
+
- Reference pages (ingest_html / ingest_url): use detail:'full' for clone-faithful rebuilds; rebuild sizes and colors from design_tokens/palette and copy typography/text_styles exactly; rebuild each widget VERBATIM as ONE html-box (inline its css). Pass base_url to ingest_html when the HTML came from a live site. If ingest_url warns the body was empty (client-rendered), work from a screenshot instead. An absolute-canvas result's \`source\` saves straight to create_page; patch the items in clone_notes afterwards with patch_page. When canvas_summary.truncated is true, re-call with sections:[id] and append each section's source.page[0] via add_section.
|
|
178
|
+
- Images: reference images (ingest images/background_images/og_image) and images the user supplies are the user's assets for BOTH intents — re-host them with upload_images (or let the save auto-host remote URLs) and never swap them for stock photos; use search_images only for slots with no source image, batching all slots in one \`queries\` call and picking the variant whose width matches the slot (size_guide). Pass a user's LOCAL file paths straight to upload_images — never upload them to a third-party host (catbox, imgur, transfer.sh…) to get a URL. Wait for the returned images map and use every hosted URL; never placeholder a slot whose upload succeeded. Settle the organization with the user first (same org as the page) — on reason:'organization_required', ask and re-call with organization_id.
|
|
179
|
+
- Icons: for each get_icon_svg result, build a SQUARE rectangle element with the svg in BOTH responsive.desktop.config.svgMask and responsive.mobile.config.svgMask and styles.background = the icon color (without a background the icon is blank).
|
|
180
|
+
- Saving: on a failed create_page / add_section / update_page, fix only the reported elements with patch_page({ draft_id, patches }) instead of rebuilding; after a timeout, retry with patch_page({ draft_id }) and no patches. To edit an existing page: find_pages → get_page → patch_page (preferred) or update_page. Pass force:true to update_page/patch_page only after the user explicitly agrees to discard changes made in the editor. update_page / patch_page / add_section save the SOURCE only — run publish_page afterwards so the preview and live page show the edit.
|
|
181
|
+
- Visual check: screenshot the preview with your OWN browser/screenshot ability when you have one (fresh and unlimited); use render_preview only as the fallback. If it returns ok:false (rate-limited), skip the visual check that round rather than failing. Do it promptly after create_page/publish_page — a domain-less preview expires ~10 minutes after the last publish; re-publish before re-checking a stale page.
|
|
182
|
+
- Publishing: a page without a custom domain has no lasting public URL — tell the user to attach a domain (publish_page with custom_domain) for a permanent link.`;
|
package/dist/http.js
CHANGED
|
@@ -497,6 +497,11 @@ export async function startHttpServer(port) {
|
|
|
497
497
|
await transport.handleRequest(req, res, body);
|
|
498
498
|
return;
|
|
499
499
|
}
|
|
500
|
+
// Unknown session (e.g. sessions are in-memory and the server restarted):
|
|
501
|
+
// the spec requires 404 so the client re-initializes. A 400 here left
|
|
502
|
+
// claude.ai "connected" with no tools after every redeploy.
|
|
503
|
+
if (sessionId)
|
|
504
|
+
return rpcError(res, 404, "Session not found (expired or server restarted) — send a new initialize request.");
|
|
500
505
|
// New session: only a POST `initialize` may open one.
|
|
501
506
|
if (req.method === "POST") {
|
|
502
507
|
const body = await readBody(req);
|
|
@@ -194,14 +194,33 @@ export async function fetchHtml(url, opts = {}) {
|
|
|
194
194
|
const ctrl = new AbortController();
|
|
195
195
|
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? FETCH_TIMEOUT_MS);
|
|
196
196
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
197
|
+
// Same SSRF policy as fetchCss: on the remote transport the URL is caller-chosen,
|
|
198
|
+
// so no private/loopback/link-local hosts, and redirects are followed MANUALLY so
|
|
199
|
+
// every hop is re-checked (RENDER_ALLOW_PRIVATE=1 opts out, e.g. a local dev page).
|
|
200
|
+
let target = url;
|
|
201
|
+
let res;
|
|
202
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
203
|
+
const allowed = isAllowedScreenshotUrl(target);
|
|
204
|
+
if (!allowed.ok)
|
|
205
|
+
return { ok: false, error: `URL not allowed: ${allowed.error ?? "private or non-public host"}` };
|
|
206
|
+
res = await fetch(target, {
|
|
207
|
+
headers: {
|
|
208
|
+
"User-Agent": opts.userAgent ?? "Mozilla/5.0 (compatible; webcake-landing-mcp/ingest_url)",
|
|
209
|
+
Accept: "text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.5",
|
|
210
|
+
},
|
|
211
|
+
signal: ctrl.signal,
|
|
212
|
+
redirect: "manual",
|
|
213
|
+
});
|
|
214
|
+
if (res.status < 300 || res.status >= 400)
|
|
215
|
+
break;
|
|
216
|
+
const loc = res.headers.get("location");
|
|
217
|
+
if (!loc)
|
|
218
|
+
return { ok: false, status: res.status, error: `Redirect ${res.status} without a Location header` };
|
|
219
|
+
target = new URL(loc, target).href;
|
|
220
|
+
res = undefined;
|
|
221
|
+
}
|
|
222
|
+
if (!res)
|
|
223
|
+
return { ok: false, error: `Too many redirects (> ${MAX_REDIRECTS})` };
|
|
205
224
|
if (!res.ok)
|
|
206
225
|
return { ok: false, status: res.status, error: `Server returned ${res.status}` };
|
|
207
226
|
const ctype = res.headers.get("content-type") ?? "";
|
package/dist/smoke.js
CHANGED
|
@@ -10,7 +10,7 @@ import { pkgVersion } from "./server.js";
|
|
|
10
10
|
import { validatePage, pageSchema } from "./domains/landing/validate.js";
|
|
11
11
|
import { expandSource } from "./core/expand.js";
|
|
12
12
|
import { compactSource, deepEq, sparseTemplate } from "./core/compact.js";
|
|
13
|
-
import { parseHtml, extractTailwindConfig, extractFontFaces, applyCloneFonts, googleWeightLinks, withGoogleWeightLinks, toFontGroups, stylesheetTags, fetchStylesheets, parseCssRules, resolveCssVars } from "./persistence/html-ingest.js";
|
|
13
|
+
import { fetchHtml, parseHtml, extractTailwindConfig, extractFontFaces, applyCloneFonts, googleWeightLinks, withGoogleWeightLinks, toFontGroups, stylesheetTags, fetchStylesheets, parseCssRules, resolveCssVars } from "./persistence/html-ingest.js";
|
|
14
14
|
import { registerFontGroups } from "./persistence/webcake-client.js";
|
|
15
15
|
import { warningsField } from "./mcp/response.js";
|
|
16
16
|
import { readConfig, resolveEnv, ENV_NAMES, configFromHeaders } from "./persistence/config.js";
|
|
@@ -710,6 +710,8 @@ console.log("== fonts: review hardening (cascade order, media, rem root, UA, Tai
|
|
|
710
710
|
// SSRF: stylesheet hrefs chosen by the crawled page never reach private hosts.
|
|
711
711
|
const priv = await fetchStylesheets(`<link rel="stylesheet" href="http://169.254.169.254/latest/meta-data"><link rel="stylesheet" href="http://127.0.0.1:1/a.css"><style>@import url(http://10.0.0.1/x.css);</style>`, "https://example.com/");
|
|
712
712
|
check("ssrf: private/link-local stylesheet + @import targets are not fetched", priv.length === 0, priv);
|
|
713
|
+
const privPage = await Promise.all(["http://169.254.169.254/latest/meta-data", "http://127.0.0.1:1/", "http://10.0.0.1/"].map((u) => fetchHtml(u)));
|
|
714
|
+
check("ingest_url SSRF: private/loopback/link-local page fetch blocked before any request", privPage.every((r) => !r.ok && /not allowed/.test(r.error ?? "")), privPage);
|
|
713
715
|
}
|
|
714
716
|
console.log("== settings: width_section defaulted (build host crashes without it) ==");
|
|
715
717
|
{
|
package/dist/tools/generation.js
CHANGED
|
@@ -9,7 +9,7 @@ import { text, warningsField, autoFixedField } from "../mcp/response.js";
|
|
|
9
9
|
import { objectOrJson } from "../mcp/params.js";
|
|
10
10
|
export function registerGenerationTools(server, domain) {
|
|
11
11
|
// 5) New element ------------------------------------------------------------
|
|
12
|
-
server.tool("new_element", "Returns a default element node for a type in the
|
|
12
|
+
server.tool("new_element", "Returns a default element node for a type in the sparse authoring shape: a fresh id, seeded styles for both breakpoints, and seeded specials. properties, runtime, empty events and per-breakpoint config are omitted because the server fills them from factory defaults on validate and save.", {
|
|
13
13
|
type: z.string().describe("Element type to create."),
|
|
14
14
|
name: z.string().optional().describe("Optional properties.name override (layer label)."),
|
|
15
15
|
}, { title: "New Element Node", readOnlyHint: true, openWorldHint: false }, async ({ type, name }) => {
|
|
@@ -22,7 +22,7 @@ export function registerGenerationTools(server, domain) {
|
|
|
22
22
|
return text(el);
|
|
23
23
|
});
|
|
24
24
|
// 6) New page skeleton ------------------------------------------------------
|
|
25
|
-
server.tool("new_page_skeleton", "Returns an empty
|
|
25
|
+
server.tool("new_page_skeleton", "Returns an empty, complete top-level page source { page:[], popup:[], settings, options, cartConfigs } in the Webcake editor's shape. desktopWidth (960 or 1200) and mobileWidth (420 or 360) set settings.width_section, the canvas width that all element coordinates are measured in.", {
|
|
26
26
|
mobileOnly: z.boolean().optional().describe("true if the page renders mobile-only."),
|
|
27
27
|
desktopWidth: z.union([z.literal(960), z.literal(1200)]).optional().describe("Desktop canvas width (settings.width_section.desktop). 960 (default, simple/narrow) or 1200 (wide/multi-column/editorial, or cloning a >960 reference)."),
|
|
28
28
|
mobileWidth: z.union([z.literal(420), z.literal(360)]).optional().describe("Mobile canvas width (settings.width_section.mobile). 420 (default) or 360 (to match a ~360–390 mobile design)."),
|
|
@@ -33,7 +33,7 @@ export function registerGenerationTools(server, domain) {
|
|
|
33
33
|
: undefined,
|
|
34
34
|
})));
|
|
35
35
|
// 7) Validate page ----------------------------------------------------------
|
|
36
|
-
server.tool("validate_page", "Validates a page source against the schema
|
|
36
|
+
server.tool("validate_page", "Validates a page source against the JSON schema and semantic rules: unique ids, dangling event targets, children only on container types, missing field_name, top-level types, and form-data bindings (duplicate field_name within one form; dangling promoId / connectedSurvey / connectedForm / set_field_value targets). Before validating it applies deterministic layout corrections (off-canvas boxes moved on-canvas, elements below wrapped text moved down) and lists them in auto_fixed. Returns errors (structural problems that block saving) and warnings (visible layout defects such as overlapping text, off-canvas bounds, or empty bands).", {
|
|
37
37
|
page: objectOrJson()
|
|
38
38
|
.describe("The page source object { page:[...], settings:{} } OR a JSON string of it."),
|
|
39
39
|
}, { title: "Validate Page Source", readOnlyHint: true, openWorldHint: false }, async ({ page }) => {
|
|
@@ -47,7 +47,7 @@ export function registerGenerationTools(server, domain) {
|
|
|
47
47
|
return text({ ...result, ...autoFixedField(autoFixed), ...warningsField(result.warnings) });
|
|
48
48
|
});
|
|
49
49
|
// 7b) Layout coordinates -----------------------------------------------------
|
|
50
|
-
server.tool("layout", "Computes
|
|
50
|
+
server.tool("layout", "Computes on-canvas coordinates (top/left/width/height) for a group of elements on both breakpoints. Modes: 'center' (one box centered on the canvas), 'row' (N boxes in a centered horizontal row on desktop, stacked into one column on mobile), 'grid' (N uniform cells in `cols` columns, stacked on mobile), 'stack' (a vertical list down the content column). Respects the page margins (content column 80–880 on desktop and 20–400 on mobile by default). Returns one box per item in input order, plus `notes` for off-canvas or over-wide inputs. Pure computation: no credentials, no network.", {
|
|
51
51
|
mode: z.enum(["center", "row", "grid", "stack"]).describe("Layout pattern. center=one box; row=horizontal row (stacks on mobile); grid=cols×rows (stacks on mobile); stack=vertical list."),
|
|
52
52
|
items: z
|
|
53
53
|
.array(z.object({ width: z.number(), height: z.number() }))
|
package/dist/tools/ingest.js
CHANGED
|
@@ -31,11 +31,11 @@ import { parseHtml, fetchHtml, fetchStylesheets, applyCloneFonts } from "../pers
|
|
|
31
31
|
const sectionsParam = z
|
|
32
32
|
.array(z.string())
|
|
33
33
|
.optional()
|
|
34
|
-
.describe("Absolute-canvas mode only: return
|
|
34
|
+
.describe("Absolute-canvas mode only: return only these canvas section ids (from a previous result's canvas_summary.sections[].id; 'SECTION_POPUP' selects the popups). A full-page result marked canvas_summary.truncated:true has its styles pruned to fit the size cap; per-section results carry each section's `source` untrimmed, and `source.page[0]` is a single section node.");
|
|
35
35
|
const detailParam = z
|
|
36
36
|
.enum(["compact", "full"])
|
|
37
37
|
.optional()
|
|
38
|
-
.describe("Level of detail in the returned AST.
|
|
38
|
+
.describe("Level of detail in the returned AST. 'compact' (default, ~2–5 KB): sections, top colors and fonts. 'full': adds the CSS custom-property palette (design tokens by name), background_images from stylesheets, gradients, per-section blocks (repeating card/tile/step structures with title/body/image/cta), li lists, extended paragraphs, images as { src, alt }, and per-section widgets = { hint, html, css? } — the cleaned source HTML + matching CSS of composite visuals (device mockup, chat thread, dashboard, browser frame). Both levels lift the design system from a `tailwind.config` block when present (Google Stitch / Tailwind-CDN pages): `palette` named by token (e.g. primary → #a43b38), `colors` ranked by usage, and `design_tokens` with the resolved spacing grid, corner radii and type scale (e.g. display-lg → 48px). Image URLs in the result (images, background_images, og_image) belong to the source page. Absolute-canvas builder exports return the deterministic `source` at either level.");
|
|
39
39
|
/** Folded into the response when a clone `source` is produced from an absolute-canvas export. */
|
|
40
40
|
const CLONE_NOTICE = "ABSOLUTE-CANVAS DETECTED → a deterministic clone `source` is included. It is a faithful 1:1 rebuild of the original page (exact boxes/styles/images/behaviors on the matching 420/960 canvas), NOT a layout reference — do NOT hand-rebuild from canvas_summary. SAVE IT DIRECTLY: call create_page with this `source` (dry_run:true first to validate + preview; then dry_run:false). External images in it are AUTO-HOSTED to the Webcake CDN on save — do NOT pre-run upload_images for them. Review `clone_notes` for the few lossy approximations (fixed/floating elements, svg-less shapes, skipped social-proof toasts) and patch_page them after the page exists. If a `sections` filter was used, this `source` holds only those sections — pass source.page[0] to add_section instead.";
|
|
41
41
|
/** Slim the heavy per-element canvas to a navigable summary (the `source` supersedes it). */
|
|
@@ -72,29 +72,29 @@ function withCloneSource(domain, parsed) {
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
export function registerIngestTools(server, domain) {
|
|
75
|
-
server.tool("ingest_html", "Parses an HTML string into a reference AST: title, description, og_image, language, and sections classified by role (header, hero, features, about, form, cta, gallery, testimonials, pricing, faq, footer, unknown) with headings,
|
|
75
|
+
server.tool("ingest_html", "Parses an HTML string into a reference AST for rebuilding the page in Webcake: title, description, og_image, language, and sections classified by role (header, hero, features, about, form, cta, gallery, testimonials, pricing, faq, footer, unknown), each with headings, paragraphs, images, CTAs, links, form fields and a size_hint (desktop section height in px — from CSS when explicit, else estimated). Also returns top colors, fonts, palette and background_images; for Tailwind-CDN pages (e.g. Google Stitch) the palette and `design_tokens` (spacing grid, corner radii, type scale) come from the page's tailwind.config. detail:'compact' (default, ~2–5 KB) or 'full' (up to ~25 KB: per-section blocks such as cards and steps, lists, gradients, images as { src, alt }, and `widgets` — the cleaned HTML + CSS of composite visuals such as device mockups). Fonts: `font_groups` lists self-hosted fonts from @font-face rules in the settings.fontGroups shape; `typography` and per-section `text_styles` give the computed font size (desktop and mobile), weight, family, line height and letter spacing. Absolute-canvas builder exports (LadiPage-family or Webcake-published HTML) are detected and converted deterministically into a complete Webcake page `source`, with `clone_notes` listing approximations and `canvas_summary` summarizing per-section geometry; external images in that source are re-hosted automatically when it is saved. Garbled Vietnamese text (UTF-8 read as Latin-1) is repaired, with a warning. No credentials, no writes.", {
|
|
76
76
|
html: z.string().describe("Raw HTML of a page or a section."),
|
|
77
77
|
intent: z
|
|
78
78
|
.enum(["adapt", "clone"])
|
|
79
79
|
.optional()
|
|
80
|
-
.describe("How the
|
|
80
|
+
.describe("How the result will be used. 'adapt' (default): the reference provides layout and imagery while the text is rewritten for the user's brand. 'clone': text and images stay close to the original. Absolute-canvas exports return a clone `source` either way."),
|
|
81
81
|
detail: detailParam,
|
|
82
82
|
sections: sectionsParam,
|
|
83
83
|
base_url: z
|
|
84
84
|
.string()
|
|
85
85
|
.optional()
|
|
86
|
-
.describe("The page's original URL, when the HTML was saved
|
|
86
|
+
.describe("The page's original URL, when the HTML was saved or copied from a live site. Resolves relative font and image URLs and fetches the page's external stylesheets (+ @imports), so self-hosted fonts (`font_groups`) and computed type (`typography` / `text_styles`) defined outside the HTML are included."),
|
|
87
87
|
}, { title: "Ingest HTML Reference", readOnlyHint: true, openWorldHint: true }, async ({ html, intent, detail, sections, base_url }) => {
|
|
88
88
|
const baseUrl = base_url && /^https?:\/\//i.test(base_url) ? base_url : undefined;
|
|
89
89
|
const extraCss = baseUrl ? await fetchStylesheets(html, baseUrl) : undefined;
|
|
90
90
|
return text({ intent: intent ?? "adapt", ...withCloneSource(domain, parseHtml(html, detail ?? "compact", { sections, baseUrl, extraCss })) });
|
|
91
91
|
});
|
|
92
|
-
server.tool("ingest_url", "Fetches a public webpage (GET,
|
|
92
|
+
server.tool("ingest_url", "Fetches a public webpage (GET, 10 s timeout, 2 MB cap) together with its external stylesheets and @imports, and parses it into the same reference AST as ingest_html: role-classified sections with size_hint heights, palette, design tokens, self-hosted fonts (`font_groups`), computed typography, images, and — with detail:'full' — per-section blocks, lists and widgets. Absolute-canvas builder exports (LadiPage-family or Webcake-published pages) are detected and converted deterministically into a complete Webcake page `source` with `clone_notes` and `canvas_summary`. JavaScript is not executed, so client-rendered sites (React/Vue/Next.js) may return little content; the result then carries a warning that the body was empty. Fetches are restricted to public hosts (private and link-local addresses are blocked). No credentials, no writes. Docs: https://github.com/vuluu2k/webcake-landing-mcp/blob/main/docs/tools.md", {
|
|
93
93
|
url: z.string().describe("Public HTTP(S) URL of the page to fetch."),
|
|
94
94
|
intent: z
|
|
95
95
|
.enum(["adapt", "clone"])
|
|
96
96
|
.optional()
|
|
97
|
-
.describe("How the
|
|
97
|
+
.describe("How the result will be used. 'adapt' (default): the reference provides layout and imagery while the text is rewritten for the user's brand. 'clone': text and images stay close to the original. Absolute-canvas exports return a clone `source` either way."),
|
|
98
98
|
detail: detailParam,
|
|
99
99
|
sections: sectionsParam,
|
|
100
100
|
}, { title: "Ingest URL Reference", readOnlyHint: true, openWorldHint: true }, async ({ url, intent, detail, sections }) => {
|
package/dist/tools/media.js
CHANGED
|
@@ -196,12 +196,12 @@ export function localContentType(ext, buf) {
|
|
|
196
196
|
}
|
|
197
197
|
export function registerMediaTools(server, allowLocalFiles = true) {
|
|
198
198
|
// 13) Search images ---------------------------------------------------------
|
|
199
|
-
server.tool("search_images", "Searches Pexels stock photos (
|
|
200
|
-
query: z.string().optional().describe("Single subject query
|
|
199
|
+
server.tool("search_images", "Searches Pexels stock photos (API: https://www.pexels.com/api/documentation/) by short English subject queries. Each photo includes hotlinkable URLs at several sizes, a `sizes` map with each variant's pixel dimensions, `avg_color`, and the photographer's name and attribution URL; the response also includes a `size_guide` mapping slot widths to variants (hero/banner ~940–1880px → large/large2x, card ~350px → medium, avatar → tiny). `queries: [...]` runs several searches in parallel and returns { queries: { [q]: result } } — `pick:'best'` (default) keeps only the top photo per query, `pick:'all'` the full list; `query` runs a single search. Works without configuration through a shared hosted proxy; a PEXELS_API_KEY env var or x-pexels-key header uses a personal Pexels quota.", {
|
|
200
|
+
query: z.string().optional().describe("Single subject query; returns the full result."),
|
|
201
201
|
queries: z
|
|
202
202
|
.array(z.string())
|
|
203
203
|
.optional()
|
|
204
|
-
.describe("
|
|
204
|
+
.describe("Several subject queries (typically one per image slot), run in parallel in one call."),
|
|
205
205
|
per_page: z.number().int().min(1).max(80).optional().describe("Photos per query (default 5)."),
|
|
206
206
|
pick: z
|
|
207
207
|
.enum(["best", "all"])
|
|
@@ -261,7 +261,7 @@ export function registerMediaTools(server, allowLocalFiles = true) {
|
|
|
261
261
|
return text({ queries: out, size_guide: IMAGE_SIZE_GUIDE });
|
|
262
262
|
});
|
|
263
263
|
// 13b) Resolve icon-font names to real inline SVGs --------------------------
|
|
264
|
-
server.tool("get_icon_svg", "Resolves icon-font
|
|
264
|
+
server.tool("get_icon_svg", "Resolves icon-font names into inline SVG markup via the public Iconify API (https://iconify.design/docs/api/). Accepts Material Symbols refs (\"ms:verified\"), Font Awesome refs (\"fa:chart-line\"), Iconify ids (\"mdi:home\"), or bare names (treated as Material Symbols); underscores become hyphens, and Material Symbols resolve to the outlined variant with a filled fallback. These are the same `ms:` / `fa:` refs that ingest results report in block.icon. Returns { icons: { \"<ref>\": { ok, svg, iconify } } }. In Webcake an icon renders as an SVG mask on a rectangle element: the SVG goes in responsive.desktop.config.svgMask and responsive.mobile.config.svgMask (each breakpoint is read separately), the visible color comes from styles.background (the SVG's own fill is ignored), and the mask is stretched to the box (preserveAspectRatio='none'), so a square box keeps its proportions. No Webcake credentials needed.", {
|
|
265
265
|
icons: z
|
|
266
266
|
.array(z.string())
|
|
267
267
|
.min(1)
|
|
@@ -291,11 +291,11 @@ export function registerMediaTools(server, allowLocalFiles = true) {
|
|
|
291
291
|
});
|
|
292
292
|
});
|
|
293
293
|
// 13c) Render a page/URL to a screenshot the model can SEE --------------------
|
|
294
|
-
server.tool("render_preview", "Renders a
|
|
294
|
+
server.tool("render_preview", "Renders a public URL to a PNG screenshot and returns it as an image. `page_id` targets a created page's preview URL (/preview/<id>); `url` targets any public page and takes precedence. full_page (default true) captures the whole scrollable page and `width` sets the viewport; `tiles:true` returns a tall page as several top-to-bottom band images (requires a self-hosted Playwright host via RENDER_SCREENSHOT_BASE, otherwise a single image is returned). Engine: Microlink's free tier by default (API: https://microlink.io/docs/api/getting-started/overview; about 50 requests/day per IP — when rate-limited the result is ok:false with HTTP 429), or a self-hosted screenshot proxy set by RENDER_SCREENSHOT_BASE / x-render-screenshot-base; MICROLINK_API_KEY / x-microlink-key raises the quota. A page published without a custom domain serves its preview for about 10 minutes after the last publish; after that the preview shows 'Preview page is expired'. Docs: https://github.com/vuluu2k/webcake-landing-mcp/blob/main/docs/tools.md", {
|
|
295
295
|
page_id: z.string().optional().describe("A created page's id — screenshots its /preview/<id> URL (built from the preview base). Provide page_id OR url."),
|
|
296
296
|
url: z.string().optional().describe("Any public http(s) URL to screenshot (e.g. the reference page being cloned). Wins over page_id."),
|
|
297
297
|
full_page: z.boolean().optional().describe("Capture the whole scrollable page (default true) vs just the viewport."),
|
|
298
|
-
width: z.number().int().min(320).max(2560).optional().describe("Viewport width in px (default 1280;
|
|
298
|
+
width: z.number().int().min(320).max(2560).optional().describe("Viewport width in px (default 1280; ~960 or 1200 matches the desktop page canvas, ~420 the mobile one)."),
|
|
299
299
|
tiles: z.boolean().optional().describe("Tall pages: return the page as MULTIPLE top→bottom band images (each readable in detail) instead of one squished full-page image. Requires a Playwright host (RENDER_SCREENSHOT_BASE); without one it falls back to a single image."),
|
|
300
300
|
}, { title: "Render Preview Screenshot", readOnlyHint: true, openWorldHint: true }, async ({ page_id, url, full_page, width, tiles }, extra) => {
|
|
301
301
|
const headers = extra?.requestInfo?.headers;
|
|
@@ -347,7 +347,7 @@ export function registerMediaTools(server, allowLocalFiles = true) {
|
|
|
347
347
|
return image(r.dataBase64, r.mimeType ?? "image/png", `Rendered ${target} (${r.bytes} bytes, via ${r.via}). Compare this to the reference: check section order, colors, spacing, image placement, and text. For each mismatch, patch_page the offending element by id, re-publish, then render_preview again until it matches.`);
|
|
348
348
|
});
|
|
349
349
|
// 14) Upload images to Webcake -----------------------------------------------
|
|
350
|
-
server.tool("upload_images", "
|
|
350
|
+
server.tool("upload_images", "Re-hosts images on Webcake's CDN (statics.pancake.vn) and returns a map from each original entry to its hosted URL. Accepts up to 20 entries per call, processed in parallel, up to 200 MB each: http(s) image URLs, data:image/…;base64 URIs, and — only when the server runs locally over stdio — local file paths (absolute, ~/, file://, or Windows drive paths; rejected per entry on the remote HTTP transport). Upload target: with Webcake credentials and a resolved organization, each image is filed into that organization's media collection (the library the Webcake editor's media picker shows) and marked collection:true with its asset_id; without credentials the public CDN endpoint is used (the URL works, but the image is not in any collection: collection:false). Organization resolution: organization_id, then WEBCAKE_ORG_ID / x-webcake-org-id, then automatic selection when the account has exactly one organization; with two or more and none given, the result is ok:false with reason:'organization_required' and the organization list. dry_run defaults to false, because this only adds images to the media library and changes no page data; dry_run:true lists what would be processed without any network or filesystem access.", {
|
|
351
351
|
urls: z
|
|
352
352
|
.array(z.string())
|
|
353
353
|
.min(1)
|
|
@@ -364,7 +364,7 @@ export function registerMediaTools(server, allowLocalFiles = true) {
|
|
|
364
364
|
organization_id: z
|
|
365
365
|
.string()
|
|
366
366
|
.optional()
|
|
367
|
-
.describe("Organization whose media collection the images
|
|
367
|
+
.describe("Organization whose media collection receives the images — normally the organization the page lives in. Optional when WEBCAKE_ORG_ID / x-webcake-org-id is set or the account has exactly one organization; with two or more and none given, the result is reason:'organization_required' with the organization list. Ignored without credentials (public CDN fallback)."),
|
|
368
368
|
dry_run: z
|
|
369
369
|
.boolean()
|
|
370
370
|
.optional()
|
|
@@ -175,19 +175,19 @@ export function registerPersistenceTools(server, domain) {
|
|
|
175
175
|
return text(await listOrganizations(config));
|
|
176
176
|
});
|
|
177
177
|
// 9) Create page (persist) --------------------------------------------------
|
|
178
|
-
server.tool("create_page", "
|
|
178
|
+
server.tool("create_page", "Creates a new page in the user's Webcake account from a page source { page, popup, settings, options, cartConfigs }, saves it, and then publishes it (builds the rendered app on the Webcake build host and publishes it through the editor's publish route) so its preview renders; publish:false skips publishing, and a failed publish does not fail the create (result.publish reports the outcome). Before validating it applies deterministic layout corrections (off-canvas boxes, elements below wrapped text, containers grown to fit) and lists them in auto_fixed. External images and self-hosted font files in the source are re-hosted on Webcake's CDN during the save. dry_run defaults to true: it validates, caches the source under a draft_id, and returns the request it would send with the token masked; dry_run:false performs the create. A draft_id (from a validation failure, dry run, or timed-out create) re-runs from the cached source. Organization: explicit organization_id ('personal' = no organization), then WEBCAKE_ORG_ID / x-webcake-org-id, then automatic — no organizations → personal, exactly one → selected (organization_auto_selected:true), two or more → ok:false with the organization list. A page without a custom domain has a preview link that renders for about 10 minutes after each publish. Requires Webcake credentials.", {
|
|
179
179
|
source: objectOrJson()
|
|
180
180
|
.optional()
|
|
181
|
-
.describe("Page source { page, popup, settings, options, cartConfigs } (object or JSON string). Required unless draft_id is given.
|
|
181
|
+
.describe("Page source { page, popup, settings, options, cartConfigs } (object or JSON string). Required unless draft_id is given. Elements may be sparse — id, type, responsive.<bp>.styles for both breakpoints, specials and real events; omitted properties/runtime/empty events and children/per-breakpoint config are filled from factory defaults (full nodes also work)."),
|
|
182
182
|
draft_id: z
|
|
183
183
|
.string()
|
|
184
184
|
.optional()
|
|
185
|
-
.describe("A draft_id from a previous create_page call (validation failure, dry_run=true, or a timed-out/failed create). Loads the cached source
|
|
185
|
+
.describe("A draft_id from a previous create_page call (validation failure, dry_run=true, or a timed-out/failed create). Loads the cached source, so the full JSON is not re-sent; covers the dry-run → real call step, fixes after errors, and retries after a timeout."),
|
|
186
186
|
name: z.string().optional().describe("Page name (default 'AI Page')."),
|
|
187
187
|
organization_id: z
|
|
188
188
|
.union([z.string(), z.number()])
|
|
189
189
|
.optional()
|
|
190
|
-
.describe("Organization to create the page in (
|
|
190
|
+
.describe("Organization to create the page in (an organization id). The string 'personal' saves without any organization and skips auto-resolution. When omitted: WEBCAKE_ORG_ID / x-webcake-org-id, else automatic — one organization is selected, two or more return the list (ok:false), none means personal."),
|
|
191
191
|
dry_run: z
|
|
192
192
|
.boolean()
|
|
193
193
|
.optional()
|
|
@@ -450,7 +450,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
450
450
|
// not limited to the 50 most-recent). If that route is missing (older backend
|
|
451
451
|
// → 404) it FALLS BACK to listing pages and filtering client-side by name/id
|
|
452
452
|
// (domain search is unavailable in the fallback — list_pages omits domains).
|
|
453
|
-
server.tool("find_pages", "Searches the account's pages by name, domain
|
|
453
|
+
server.tool("find_pages", "Searches the account's pages by name, domain and/or page id. Filters are AND-combined; name and domain are case-insensitive substrings, and domain matches custom_domain or default_domain. Each result includes id, name, organization_id, custom_domain, default_domain and updated_at. With no filters it returns the most recently updated pages. Requires Webcake credentials.", {
|
|
454
454
|
name: z.string().optional().describe("Case-insensitive substring of the page name to match."),
|
|
455
455
|
domain: z
|
|
456
456
|
.string()
|
|
@@ -487,7 +487,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
487
487
|
});
|
|
488
488
|
});
|
|
489
489
|
// 11) Get page (read source) ------------------------------------------------
|
|
490
|
-
server.tool("get_page", "Fetches an existing page's
|
|
490
|
+
server.tool("get_page", "Fetches an existing page's source tree { page, popup, settings, options, cartConfigs } with its name, organization_id and source_version. By default the tree is compacted: boilerplate shared by every element (properties, runtime, empty events/children, per-breakpoint config and factory-default style keys) is stripped, giving the same sparse shape the save tools accept and re-hydrate. compact:false returns the raw stored tree. Requires Webcake credentials.", {
|
|
491
491
|
page_id: z.string().describe("The page id (from list_pages or a URL)."),
|
|
492
492
|
compact: z
|
|
493
493
|
.boolean()
|
|
@@ -516,7 +516,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
516
516
|
});
|
|
517
517
|
});
|
|
518
518
|
// 12) Update page (edit existing) -------------------------------------------
|
|
519
|
-
server.tool("update_page", "
|
|
519
|
+
server.tool("update_page", "Replaces an existing page's stored source with an edited tree (source only; the published build is not refreshed). Validates first. Concurrency check: the save is refused with page_changed_externally when the page was edited elsewhere since it was last read (base_version, or the version the server recorded), or with unverified_overwrite when an unverified save would delete live element ids; force:true overrides. dry_run defaults to true: it validates, caches the source under a draft_id and returns the masked request; dry_run:false saves. A draft_id from a dry run or a failed/timed-out update re-runs from the cached source. Requires Webcake credentials.", {
|
|
520
520
|
page_id: z.string().optional().describe("The page id to update (must be owned by the account). Required unless draft_id is given (the page_id is stored in the draft)."),
|
|
521
521
|
source: objectOrJson()
|
|
522
522
|
.optional()
|
|
@@ -524,15 +524,15 @@ export function registerPersistenceTools(server, domain) {
|
|
|
524
524
|
draft_id: z
|
|
525
525
|
.string()
|
|
526
526
|
.optional()
|
|
527
|
-
.describe("A draft_id from a previous update_page call (dry_run=true or a timed-out/failed update). Loads the cached source
|
|
527
|
+
.describe("A draft_id from a previous update_page call (dry_run=true or a timed-out/failed update). Loads the cached source, so the full JSON is not re-sent; covers the dry-run → real call step and retries after a timeout."),
|
|
528
528
|
base_version: z
|
|
529
529
|
.string()
|
|
530
530
|
.optional()
|
|
531
|
-
.describe("The source_version
|
|
531
|
+
.describe("The page's source_version as returned by get_page. When it no longer matches the live page, the save is rejected rather than reverting edits made in the Webcake editor meanwhile. Optional — when omitted, the server uses the version it recorded at this session's last read or write of the page."),
|
|
532
532
|
force: z
|
|
533
533
|
.boolean()
|
|
534
534
|
.optional()
|
|
535
|
-
.describe("Default false. true
|
|
535
|
+
.describe("Default false. true saves even when the page changed outside this session, discarding those outside changes."),
|
|
536
536
|
dry_run: z.boolean().optional().describe("Default TRUE — validate, cache the source as draft_id, and preview without sending. Set false to actually save."),
|
|
537
537
|
}, { title: "Update Webcake Page (Overwrite)", readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async ({ page_id, source, draft_id, base_version, force, dry_run }, extra) => {
|
|
538
538
|
const isDry = dry_run !== false;
|
|
@@ -695,15 +695,15 @@ export function registerPersistenceTools(server, domain) {
|
|
|
695
695
|
return Array.isArray(v) ? v : [v];
|
|
696
696
|
};
|
|
697
697
|
const sectionLabel = (s) => s?.properties?.name ?? s?.specials?.name ?? s?.id ?? s?.type ?? "section";
|
|
698
|
-
server.tool("add_section", "Appends one or more
|
|
698
|
+
server.tool("add_section", "Appends one or more section nodes to the end of an existing page's `page` array, server-side: only the new sections are sent, and the backend rejects element ids that already exist on the page. The sections are validated first. dry_run defaults to true: it validates, caches the payload under a draft_id and previews the request; dry_run:false appends. A validation failure also returns a draft_id, which patch_page accepts for element-level fixes. Section-level saves cannot change page settings, so font weights that need new font links are reported in font_weights_notice. Requires Webcake credentials.", {
|
|
699
699
|
page_id: z.string().describe("The page id to append to (from create_page or list_pages; must be owned by the account)."),
|
|
700
700
|
sections: objectsOrJson()
|
|
701
701
|
.optional()
|
|
702
|
-
.describe("One section node, or an array of section nodes,
|
|
702
|
+
.describe("One section node, or an array of section nodes, appended to the end of `page` (object/array or JSON string). Each is a normal section element { id, type:'section', responsive, children, … } with a unique id; sections stack vertically after the existing ones. Nodes may be sparse — omitted properties/runtime/empty events and children/per-breakpoint config are filled from factory defaults. Required unless draft_id is supplied."),
|
|
703
703
|
draft_id: z
|
|
704
704
|
.string()
|
|
705
705
|
.optional()
|
|
706
|
-
.describe("A draft id returned by a previous add_section call (dry_run=true or validation failure). Loads the cached section payload
|
|
706
|
+
.describe("A draft id returned by a previous add_section call (dry_run=true or validation failure). Loads the cached section payload, so the sections are not re-sent."),
|
|
707
707
|
dry_run: z
|
|
708
708
|
.boolean()
|
|
709
709
|
.optional()
|
|
@@ -981,12 +981,12 @@ export function registerPersistenceTools(server, domain) {
|
|
|
981
981
|
}
|
|
982
982
|
return touched;
|
|
983
983
|
};
|
|
984
|
-
server.tool("patch_page", "Edits a page by element id
|
|
985
|
-
page_id: z.string().optional().describe("
|
|
984
|
+
server.tool("patch_page", "Edits a page by element id without sending the whole source. The target is either a live page (page_id) or a cached draft (draft_id) returned by a failed or dry-run create_page, add_section or update_page call. Ops: {op:'update', id, type?, specials?, styles?:{desktop?,mobile?}, config?:{desktop?,mobile?}, events?, properties?} shallow-merges fields (op defaults to 'update'; it cannot delete existing keys), {op:'replace', id, element} swaps the node, {op:'remove', id} deletes it, and {op:'add', parent_id, element} appends a child to a container; `element` may be sparse. The server loads the source, applies the ops, validates the whole merged tree (errors block the save) and saves: a live page is updated in place, a create draft becomes a new page, a section draft is appended, and an update draft is re-saved. With a draft_id and no patches, the cached draft is committed as-is (the retry after a timeout). dry_run defaults to true (load, merge, validate, preview; no write); dry_run:false saves. Requires Webcake credentials; a page_id patch reads the live page, so it needs them even on a dry run.", {
|
|
985
|
+
page_id: z.string().optional().describe("A live page id to edit (must be owned by the account). Provide page_id OR draft_id; with a sections or update draft_id, page_id overrides the stored target page."),
|
|
986
986
|
draft_id: z.string().optional().describe("Commit or fix a CACHED source: from create_page (failed/timed-out → new page created once valid), add_section (dry_run or failure → sections appended), or update_page/live-page patch (timed-out/failed → updatePageSource retried). The originating tool's error/dry_run response returns draft_id. Provide page_id OR draft_id. Empty/omitted patches = commit the cached draft as-is (the RETRY PATH after a timeout)."),
|
|
987
987
|
patches: objectsOrJson()
|
|
988
988
|
.optional()
|
|
989
|
-
.describe("One op object or an array of them (object/array or JSON string). Each targets an element by id: {op:'update',id,type?,specials?,styles?:{desktop?,mobile?},config?:{desktop?,mobile?},events?,properties?} merges fields into the element (op may be omitted;
|
|
989
|
+
.describe("One op object or an array of them (object/array or JSON string). Each targets an element by id: {op:'update',id,type?,specials?,styles?:{desktop?,mobile?},config?:{desktop?,mobile?},events?,properties?} merges fields into the element (op may be omitted; `type` changes the element type; a merge cannot delete an existing key, so schema 'must NOT have additional properties' errors are fixed with op:'replace' and a clean node); {op:'replace',id,element} swaps the node; {op:'remove',id} deletes it; {op:'add',parent_id,element} appends a child to a container. `element` may be a sparse node (id/type/styles/specials/events); omitted boilerplate is filled from factory defaults. Empty or omitted with a draft_id: the cached draft is committed as-is."),
|
|
990
990
|
force: z
|
|
991
991
|
.boolean()
|
|
992
992
|
.optional()
|
|
@@ -1400,7 +1400,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
1400
1400
|
});
|
|
1401
1401
|
});
|
|
1402
1402
|
// 15) Publish page (go live) -------------------------------------------------
|
|
1403
|
-
server.tool("publish_page", "Publishes an
|
|
1403
|
+
server.tool("publish_page", "Publishes an existing page: builds the rendered app on the Webcake build host (POST <buildBase>/render/build; https://build.webcake.io in production, overridable with WEBCAKE_BUILD_BASE / x-webcake-build-base) and then creates or updates the published record that public serving reads. With custom_domain (already pointed at Webcake) the page is served at that domain; when omitted, the page's currently attached domain is reused; with no domain, or custom_domain:'', only the preview link (<previewBase>/preview/<page_id>) is returned, and it renders for about 10 minutes after the publish. Without a build host, or if the build fails, it falls back to a source-only publish that saves a version but serves nothing, and returns a warning. dry_run defaults to true and makes no network calls; dry_run:false builds and publishes. Requires Webcake credentials.", {
|
|
1404
1404
|
page_id: z.string().describe("The page id to publish (must be owned by the account)."),
|
|
1405
1405
|
custom_domain: z
|
|
1406
1406
|
.string()
|
package/dist/tools/reference.js
CHANGED
|
@@ -25,12 +25,12 @@ export function registerReferenceTools(server, domain) {
|
|
|
25
25
|
return text({ total: domain.elementTypes.length, categories: byCategory });
|
|
26
26
|
});
|
|
27
27
|
// 3) Get element ------------------------------------------------------------
|
|
28
|
-
server.tool("get_element", "Returns
|
|
29
|
-
type: z.string().optional().describe("Single element type
|
|
28
|
+
server.tool("get_element", "Returns usage details for one element type, or for several via `types: [...]`: a summary, when it applies, key `specials` fields, a sparse skeleton node (the server fills omitted boilerplate), and for common types a filled example. `types` returns { elements: { [type]: details } }; `type` returns a single doc.", {
|
|
29
|
+
type: z.string().optional().describe("Single element type."),
|
|
30
30
|
types: z
|
|
31
31
|
.array(z.string())
|
|
32
32
|
.optional()
|
|
33
|
-
.describe("
|
|
33
|
+
.describe("Several element types fetched in one call, e.g. ['section','text-block','button','form','input']."),
|
|
34
34
|
}, { title: "Get Element Details", readOnlyHint: true, openWorldHint: false }, async ({ type, types }) => {
|
|
35
35
|
const list = types && types.length ? types : type ? [type] : [];
|
|
36
36
|
if (list.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.10",
|
|
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",
|