webcake-landing-mcp 1.0.56 → 1.0.57

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
@@ -152,6 +152,11 @@ npx -y webcake-landing-mcp login # opens the browser once, saves the token to
152
152
 
153
153
  …or set `WEBCAKE_ENV` (`local` | `staging` | `prod` — fills in all base URLs) + `WEBCAKE_JWT`.
154
154
 
155
+ For `publish_page` to produce a **rendered** (non-blank) page, a build host is needed:
156
+ - `prod` preset auto-configures `https://build.webcake.io` — no extra setup.
157
+ - For staging/local, set `WEBCAKE_BUILD_BASE=<url>` or send the `x-webcake-build-base` header per request.
158
+ - Without it, `publish_page` falls back to source-only with `rendered:false` + a warning.
159
+
155
160
  Everything else — the full env-var table, environment presets, per-request headers for the hosted
156
161
  server, the `login` browser flow (+ backend contract), and how to grab a JWT by hand — lives in
157
162
  **[docs/configuration.md](docs/configuration.md)**.
@@ -164,7 +169,7 @@ server, the `login` browser flow (+ backend contract), and how to grab a JWT by
164
169
  |-------|---------------|
165
170
  | **[Connect your IDE / claude.ai](docs/connect-mcp.md)** | Step-by-step connection for every client (npx & hosted URL), troubleshooting table. |
166
171
  | **[Configuration](docs/configuration.md)** | Env vars, `--env` presets, browser `login`, per-request headers, getting a JWT. |
167
- | **[Tools reference](docs/tools.md)** | All 19 tools in detail + the step-by-step workflow + model notes. |
172
+ | **[Tools reference](docs/tools.md)** | All 20 tools in detail + the step-by-step workflow + model notes. |
168
173
  | **[Usage examples](docs/usage-examples.md)** | Three end-to-end walkthroughs: build from a brief, surgical edit, inspect a type. |
169
174
  | **[Manual / advanced install](docs/manual-install.md)** | Shell installers, cloned builds, hand-written per-IDE config. |
170
175
  | **[Page-element schema](docs/page-element-schema.md)** | The full element-model reference (+ [every special/event](docs/element-specials-reference.md)). |
@@ -173,13 +178,13 @@ server, the `login` browser flow (+ backend contract), and how to grab a JWT by
173
178
 
174
179
  ## 🧰 The tools at a glance
175
180
 
176
- 19 tools in five groups — full descriptions in **[docs/tools.md](docs/tools.md)**:
181
+ 20 tools in five groups — full descriptions in **[docs/tools.md](docs/tools.md)**:
177
182
 
178
183
  | Group | Tools | Needs |
179
184
  |-------|-------|-------|
180
185
  | **Reference** | `get_generation_guide` · `list_elements` · `get_element` · `get_page_schema` | nothing |
181
186
  | **Generation** | `new_element` · `new_page_skeleton` · `validate_page` | nothing |
182
- | **Media** | `search_images` (real Pexels stock photos) | nothing (optional own key) |
187
+ | **Media** | `search_images` (real Pexels stock photos) · `upload_images` (re-host external images) | nothing |
183
188
  | **Ingest** | `ingest_html` · `ingest_url` (recreate an existing page) | nothing |
184
189
  | **Persistence** | `list_organizations` · `create_page` · `list_pages` · `find_pages` · `get_page` · `update_page` · `add_section` · `patch_page` · `publish_page` | `WEBCAKE_API_BASE` + `WEBCAKE_JWT` |
185
190
 
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "v": "1.0.57",
4
+ "d": "11/06/2026",
5
+ "type": "Added",
6
+ "en": "New upload_images tool re-hosts up to 20 external image URLs or data: URIs as Webcake-hosted URLs (statics.pancake.vn) by downloading and uploading…",
7
+ "vi": "Công cụ upload_images mới tải lại tối đa 20 URL ảnh ngoài hoặc data: URI thành URL do Webcake lưu trữ (statics.pancake.vn) bằng cách tải về và…"
8
+ },
2
9
  {
3
10
  "v": "1.0.56",
4
11
  "d": "11/06/2026",
@@ -33,12 +40,5 @@
33
40
  "type": "Added",
34
41
  "en": "validate_page now errors when an element type that the renderer cannot animate (any type other than group, image-block, text-block, rectangle,…",
35
42
  "vi": "validate_page nay báo lỗi khi một element có loại không được renderer hỗ trợ animation (chỉ group, image-block, text-block, rectangle, button,…"
36
- },
37
- {
38
- "v": "1.0.51",
39
- "d": "10/06/2026",
40
- "type": "Added",
41
- "en": "create_page, update_page, and add_section dry-run responses now include a draft_id, and all three tools now accept draft_id as an input parameter:…",
42
- "vi": "Các response dry-run của create_page, update_page và add_section nay đều trả về draft_id, đồng thời cả ba công cụ đều nhận draft_id làm tham số đầu…"
43
43
  }
44
44
  ]
@@ -3,7 +3,7 @@ export const CONTENT = [
3
3
  {
4
4
  type: "text-block", category: "content", container: false, defaultName: "Text",
5
5
  summary: "Text. specials.text holds the content (may contain inline HTML); specials.tag sets the semantic tag. Supports template variables ({{key}}), formula mode, URL-param injection, and date formatting.",
6
- useWhen: "Any headline, paragraph, label. Use tag h1/h2 for headings, p for body. Style via responsive.styles (fontSize, color, fontWeight, textAlign). ALWAYS set color to CONTRAST the band it sits on: near-black (e.g. rgba(26,32,44,1)) on light bands, near-white ONLY on a dark/image band — white text on a light band renders invisible. styles.background on a text-block = a GRADIENT TEXT FILL (emits -webkit-text-fill-color:transparent); you must also set styles['-webkitBackgroundClip']:'text' or the glyphs go invisible. The box background key is styles.backgroundTxt — use that for a colored box behind the text. NEVER set styles.background expecting a box fill.",
6
+ useWhen: "Any headline, paragraph, label. Use tag h1/h2 for headings, p for body. Style via responsive.styles (fontSize, color, fontWeight, textAlign). ALWAYS set color to CONTRAST the band it sits on: near-black (e.g. rgba(26,32,44,1)) on light bands, near-white ONLY on a dark/image band — white text on a light band renders invisible. styles.background on a text-block = a GRADIENT TEXT FILL (emits -webkit-text-fill-color:transparent); you must also set styles['-webkitBackgroundClip']:'text' or the glyphs go invisible. The box background key is styles.backgroundTxt — use that for a colored box behind the text. NEVER set styles.background expecting a box fill. text-block does NOT emit border-radius — for a rounded pill/badge, put a rectangle (borderRadius '13px', pill bg color) BEHIND the text-block (zIndex 2 on the text-block); the rounded shape comes from the rectangle, never from the text-block itself.",
7
7
  keySpecials: {
8
8
  text: "string — the visible text; may include inline HTML (<b>, <br>, <span style>…). Also supports template variables: {{today}}, {{yesterday}}, {{tomorrow}} (formatted dates), {{coupon_text}}, {{coupon_code}}, {{coupon_codes}}, {{spin_turn_left}}, {{cart_total_price}}, {{cart_subtotal}}, {{cart_shipping_fee}}, {{cart_discount_code}}, {{voucher_price_cart}}, {{cart_item}}, {{cart_bonus_item}}, {{form_error_log}}, {{total_cart}}. Dynamic form field binding: {{formId__fieldName}} substitutes a field value from a sibling form.",
9
9
  tag: "p | h1 | h2 | h3 | h4 | h5 | h6 | span | div.",
@@ -101,10 +101,11 @@ SECTION BUILD HINTS (apply to whichever sections the chosen archetype uses)
101
101
  - SOCIAL PROOF — testimonial cards, a logo strip, or a row of stat counters (auto-number + label). Center the row.
102
102
  - FORM / CTA — center the form box; stack inputs vertically with comfortable spacing; each input needs a unique specials.field_name (canonical: full_name, phone_number, email, address, quantity); prominent submit button.
103
103
  - FOOTER — name + real contact lines, usually centered on a dark band. Only real data the user provided.
104
+ - TAG/BADGE "PILL" recipe — a rounded rectangle (borderRadius "13px", the pill bg color) BEHIND a text-block (zIndex 2, styles.backgroundTxt for the box fill if needed, textAlign center). NEVER put the pill color in styles.background on the text-block — that key activates gradient-text-fill mode (the renderer emits -webkit-text-fill-color:transparent and the glyphs go invisible). text-block does NOT emit border-radius at all — the rounded shape must come from a rectangle behind the text.
104
105
 
105
106
  RULES
106
107
  - Visible content goes in "specials" (text-block.specials.text, image-block.specials.src…), NEVER in "styles".
107
- - Colors as rgba(r,g,b,a). fontSize/borderWidth/top/left/width/height are NUMBERS (px).
108
+ - Colors as rgba(r,g,b,a). fontSize/borderWidth/top/left/width/height are NUMBERS (px). borderRadius is a STRING with CSS units ("8px", "50%", "16px 16px 0 0") — a bare number or unit-less string is auto-coerced to px by the server, but write the unit explicitly to avoid surprises.
108
109
  - IMAGES: a real landing page has images (hero/product shot, feature icons, about photo). PREFER REAL PHOTOS: call search_images with a short English subject (e.g. 'fresh coffee cup', 'modern office team') and put a returned URL into image-block specials.src — use src.large for a hero/banner, src.medium for a card/thumb (avg_color helps pick a matching section background). ONLY if search_images returns ok:false (or is unreachable) FALL BACK to a PLACEHOLDER sized to the box: "https://placehold.co/<width>x<height>". NEVER leave src empty — it renders blank on the live page. The server automatically derives styles.background from specials.src on every expand (create/update/validate) using the editor's exact format: 'center center/ cover no-repeat scroll content-box url(<src>) border-box' — you do NOT need to set styles.background manually; if you do hand-write it, it must contain url(...). gallery.media = array of OBJECTS {type:'image', link:'<real-or-placeholder-url>', linkVideo:'', typeVideo:'youtube', imageCompression:true} (NOT plain URL strings — the gallery reads item.link); video.specials.img = a poster image (real photo, else placeholder). Do NOT set a flat (no url()) styles.background on a video element — it suppresses the poster image.
109
110
  - CONTRAST (check EVERY text element against the band it sits on, especially SATURATED / mid-tone bands like yellow, orange, teal, pink — there "light vs dark text" is not obvious, so decide by the band's luminance): light bands → near-black text (e.g. rgba(20,30,25,1)); dark bands → near-white text; a saturated/mid-tone band → whichever of near-black or near-white actually reads (for a bright yellow/amber band that means DARK text, not white/grey). NEVER use muted-grey, low-alpha (alpha < ~0.85), or near-white text on a colored band — that is exactly what makes labels look faded/sunken. Muted-grey is ONLY for secondary text on a white/very-light band. Icons and their captions follow the SAME rule as the text beside them.
110
111
  - movable:false for section/slide/grid-item/popup; otherwise true. runtime is always {}.
@@ -148,4 +149,9 @@ EDITING an existing page
148
149
  - SMALL edit → PREFER patch_page(page_id, patches): send ONLY the changed elements by id, not the whole source. Ops — {op:'update',id,specials?,styles?:{desktop?,mobile?},config?:{desktop?,mobile?},events?,properties?} (shallow-merge; op defaults to 'update'), {op:'replace',id,element}, {op:'remove',id}, {op:'add',parent_id,element}. The MCP fetches the live source, applies the ops, validates the whole tree, and saves. Reserve update_page(page_id, full source) for when you're rewriting most of the page.
149
150
  - To add an element: give it a unique id + top/left/width/height, then patch_page({op:'add', parent_id:<section id>, element:<node>}).
150
151
  - FIX-AFTER-ERROR (never rebuild the whole source): when create_page/update_page/add_section reports validation errors, fix ONLY the offending element ids with patch_page. If create_page failed, its error returns a draft_id (source cached) → patch_page({ draft_id, patches, dry_run:false }) fixes + creates the page (a wrong type → { op:'update', id, type:'<allowed type>' }). If update_page/add_section failed, the page has a page_id → patch_page({ page_id, patches }).
151
- - patch_page/update_page default to dry_run=true (preview); pass dry_run:false to save.`;
152
+ - patch_page/update_page default to dry_run=true (preview); pass dry_run:false to save.
153
+
154
+ REFERENCE INPUT (HTML page to clone or adapt)
155
+ - ingest_html(html, detail:'full') / ingest_url(url, detail:'full') returns a richer AST for clone-quality rebuilds: CSS custom-property palette (design tokens by name), background_images from stylesheets (hero/CTA bg images invisible to a plain img scan), per-section blocks (repeating card/tile/step structures with title/body/image/cta), li lists, gradients, and images as { src, alt } objects. Use detail:'compact' (default) for a quick layout-only reference.
156
+ - 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.
157
+ - When intent='clone': re-host image URLs found in the AST (images, background_images, og_image) via upload_images instead of hotlinking. For intent='adapt' (default): use the AST for layout/hierarchy only and write fresh content for the user's brand.`;
@@ -62,14 +62,52 @@ function normalizeImageBlocks(node) {
62
62
  normalizeImageBlocks(child);
63
63
  }
64
64
  }
65
- /** Apply image-block normalization to every node in a page source. */
65
+ // ---------------------------------------------------------------------------
66
+ // borderRadius normalization: the renderer emits border-radius RAW from the
67
+ // styles object (exportCss.js: `border-radius: ${style.borderRadius};`).
68
+ // A bare number (e.g. 16) or a unit-less string (e.g. "16") produces invalid
69
+ // CSS that browsers silently ignore — every corner renders square. Valid values
70
+ // are strings with CSS units: "16px", "50%", "16px 16px 0 0".
71
+ //
72
+ // Fix: after every expand pass, walk every node and, for each breakpoint whose
73
+ // styles.borderRadius is a number or a unit-less numeric string, coerce it to
74
+ // "<n>px". Already-valid strings (contain a letter or %) pass through untouched.
75
+ // ---------------------------------------------------------------------------
76
+ /** True when s is a plain number-string with no CSS unit (e.g. "16", "0"). */
77
+ function isUnitless(s) {
78
+ return /^\s*-?\d+(\.\d+)?\s*$/.test(s);
79
+ }
80
+ /** Walk a tree node and coerce numeric/unit-less borderRadius to "<n>px" in-place (mutates). */
81
+ function normalizeBorderRadius(node) {
82
+ if (!node || typeof node !== "object")
83
+ return;
84
+ for (const bp of ["desktop", "mobile"]) {
85
+ const styles = node.responsive?.[bp]?.styles;
86
+ if (!styles || typeof styles !== "object")
87
+ continue;
88
+ const br = styles.borderRadius;
89
+ if (typeof br === "number" && Number.isFinite(br)) {
90
+ styles.borderRadius = `${br}px`;
91
+ }
92
+ else if (typeof br === "string" && isUnitless(br)) {
93
+ styles.borderRadius = `${parseFloat(br)}px`;
94
+ }
95
+ }
96
+ if (Array.isArray(node.children)) {
97
+ for (const child of node.children)
98
+ normalizeBorderRadius(child);
99
+ }
100
+ }
101
+ /** Apply all post-expand normalizations to every node in a page source. */
66
102
  function normalizeSource(source) {
67
103
  if (!source || typeof source !== "object")
68
104
  return source;
69
105
  for (const arr of ["page", "popup", "dynamic_pages"]) {
70
106
  if (Array.isArray(source[arr])) {
71
- for (const node of source[arr])
107
+ for (const node of source[arr]) {
72
108
  normalizeImageBlocks(node);
109
+ normalizeBorderRadius(node);
110
+ }
73
111
  }
74
112
  }
75
113
  return source;
@@ -24,7 +24,7 @@ RULES (follow for every request):
24
24
  A wrong element type is the most common error → { op:'update', id:'<element id>', type:'<allowed type>' } (run list_elements if unsure). Drafts expire in ~30 min.
25
25
  - DRY-RUN CACHE: create_page, update_page, and add_section dry_run=true all cache the validated payload and return a draft_id. Re-run the same tool with { draft_id, dry_run:false } — no need to re-send the source.
26
26
  - Organizations: call list_organizations and ask which to use; default to the is_default org. Endpoints are owner-scoped (only the account's own pages).
27
- - REFERENCE INPUT — if the user provides a layout reference, USE it as the layout anchor (don't ignore it, don't re-invent from scratch). Three input modes: (1) IMAGE/screenshot attached in chat → analyze it natively (no tool call): identify section flow (hero/features/form/cta/footer), heading hierarchy, dominant colors, font feel, then map sections to Webcake elements. (2) HTML string → call ingest_html(html) to get a compact AST. (3) URL → call ingest_url(url) for the same AST. The AST classifies sections by role and lists headings/subheadings/ctas/images/form_fields plus brand hints (colors/fonts) — use it for LAYOUT + HIERARCHY, then generate FRESH content tailored to the user's brand (don't 1:1 copy text). intent='clone' only when the user explicitly asks to mirror the original; default intent='adapt'. The reference workflow PRESERVES craft rules above (centering, page margin, premium spacing, real images) — apply them on top of the reference layout, don't bypass them.
27
+ - REFERENCE INPUT — if the user provides a layout reference, USE it as the layout anchor (don't ignore it, don't re-invent from scratch). Three input modes: (1) IMAGE/screenshot attached in chat → analyze it natively (no tool call): identify section flow (hero/features/form/cta/footer), heading hierarchy, dominant colors, font feel, then map sections to Webcake elements using role→element hints (hero → section with background image/overlay + heading + paragraph + button; features → group blocks with icon/title/text; stats bar → group with heading+text per stat; pricing → group with text list + button; footer → section with text + links). (2) HTML string → call ingest_html(html, detail:'full') to get the richer AST when cloning; use detail:'compact' (default) for a layout-only reference. (3) URL → call ingest_url(url, detail:'full') for the same richer AST. The AST classifies sections by role and lists headings/subheadings/ctas/images/form_fields plus brand hints (colors/fonts), CSS custom-property palette, background_images from stylesheets, and in full mode: per-section blocks (cards/tiles/steps with title/body/image/cta), li lists, and gradients — use it for LAYOUT + HIERARCHY, then generate FRESH content tailored to the user's brand (don't 1:1 copy text). When cloning (intent='clone'), re-host image URLs found in the AST (images, background_images, og_image) via upload_images instead of hotlinking. intent='clone' only when the user explicitly asks to mirror the original; default intent='adapt'. The reference workflow PRESERVES craft rules above (centering, page margin, premium spacing, real images) — apply them on top of the reference layout, don't bypass them.
28
28
 
29
29
  MODEL (essentials):
30
30
  - Top-level: { page:[sections], popup:[popups], dynamic_pages:[], settings:{}, options:{mobileOnly,versionID}, cartConfigs:{isActive:false}, svariations:[] }. Popups are a SEPARATE top-level array, NOT inside page; currency lives in settings.currency (not options). Leave dynamic_pages/svariations as [] for a static page, but keep them on edit round-trips.
@@ -39,4 +39,4 @@ MODEL (essentials):
39
39
 
40
40
  - PREVIEW vs PUBLISH: the preview_url returned by create_page/update_page/add_section lives on the PREVIEW host (preview.localhost:5800 local / staging.webcake.me staging / www.webcake.me prod — NOT the builder subdomain) and renders the stored source immediately — share it as-is for review. When the user wants the page LIVE (public/published, optionally on their custom domain), call publish_page({ page_id, custom_domain?, custom_path?, dry_run:false }).
41
41
 
42
- Start by calling get_generation_guide. Tools: get_generation_guide, list_elements, get_element, new_element, new_page_skeleton, get_page_schema, validate_page, search_images, ingest_html, ingest_url, list_organizations, create_page, list_pages, find_pages, get_page, update_page, add_section, patch_page, publish_page.`;
42
+ Start by calling get_generation_guide. Tools: get_generation_guide, list_elements, get_element, new_element, new_page_skeleton, get_page_schema, validate_page, search_images, upload_images, ingest_html, ingest_url, list_organizations, create_page, list_pages, find_pages, get_page, update_page, add_section, patch_page, publish_page.`;
@@ -39,9 +39,9 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
39
39
  * after create/update (a distinct host — NOT the API and NOT the SPA).
40
40
  */
41
41
  export const ENVIRONMENTS = {
42
- local: { apiBase: "http://localhost:5800", appBase: "http://localhost:5173", builderBase: "http://builder.localhost:5800", previewBase: "http://preview.localhost:5800" },
43
- staging: { apiBase: "https://api.staging.webcake.io", appBase: "https://staging.webcake.io", builderBase: "https://builder.staging.webcake.io", previewBase: "https://staging.webcake.me" },
44
- prod: { apiBase: "https://api.webcake.io", appBase: "https://webcake.io", builderBase: "https://builder.webcake.io", previewBase: "https://www.webcake.me" },
42
+ local: { apiBase: "http://localhost:5800", appBase: "http://localhost:5173", builderBase: "http://builder.localhost:5800", previewBase: "http://preview.localhost:5800", buildBase: undefined },
43
+ staging: { apiBase: "https://api.staging.webcake.io", appBase: "https://staging.webcake.io", builderBase: "https://builder.staging.webcake.io", previewBase: "https://staging.webcake.me", buildBase: undefined },
44
+ prod: { apiBase: "https://api.webcake.io", appBase: "https://webcake.io", builderBase: "https://builder.webcake.io", previewBase: "https://www.webcake.me", buildBase: "https://build.webcake.io" },
45
45
  };
46
46
  export function stripTrailingSlash(s) {
47
47
  return s?.replace(/\/+$/, "");
@@ -104,6 +104,13 @@ export function readConfig(overrides = {}) {
104
104
  preset?.previewBase ??
105
105
  saved.previewBase ??
106
106
  "https://www.webcake.me");
107
+ // The build host is used by publish_page to build app/app_css before publishing.
108
+ // Prod preset has https://build.webcake.io; staging/local have no reliable public
109
+ // host so they leave it undefined — callers must set WEBCAKE_BUILD_BASE explicitly.
110
+ const buildBase = stripTrailingSlash(overrides.buildBase ??
111
+ process.env.WEBCAKE_BUILD_BASE ??
112
+ preset?.buildBase ??
113
+ saved.buildBase);
107
114
  return {
108
115
  config: {
109
116
  base: cleanBase,
@@ -112,6 +119,7 @@ export function readConfig(overrides = {}) {
112
119
  appBase: stripTrailingSlash(overrides.appBase ?? process.env.WEBCAKE_APP_BASE ?? preset?.appBase ?? saved.appBase),
113
120
  builderBase,
114
121
  previewBase,
122
+ buildBase,
115
123
  },
116
124
  missing: [],
117
125
  };
@@ -142,6 +150,7 @@ export function configFromHeaders(headers) {
142
150
  appBase: header(headers, "x-webcake-app-base"),
143
151
  builderBase: header(headers, "x-webcake-builder-base"),
144
152
  previewBase: header(headers, "x-webcake-preview-base"),
153
+ buildBase: header(headers, "x-webcake-build-base"),
145
154
  env: header(headers, "x-webcake-env"),
146
155
  };
147
156
  }