webcake-landing-mcp 1.0.49 → 1.0.50
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 +9 -3
- package/dist/changelog.json +7 -7
- package/dist/domains/landing/instructions.js +3 -1
- package/dist/persistence/config.js +21 -5
- package/dist/persistence/webcake-client.js +140 -3
- package/dist/smoke.js +20 -3
- package/dist/tools/persistence.js +48 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ persists it (source-only — the page opens in the editor where re-saving render
|
|
|
89
89
|
| **npx (local)** — runs on your machine | Personal daily use, full control | browser `login`, a JWT, or none (reference tools) |
|
|
90
90
|
| **Hosted URL** — use our live server, nothing to install | No Node.js, teams, the claude.ai dialog | your personal `?jwt=` link / `x-webcake-jwt` header |
|
|
91
91
|
|
|
92
|
-
The **reference + generation tools** (`get_generation_guide`, `list_elements`, `validate_page`, …) and the **ingest tools** (`ingest_html`, `ingest_url` — turn an existing HTML or URL into a layout anchor so the AI can recreate or adapt it) work with **zero config**; only the **persistence tools** (`create_page`, `update_page`, `add_section`, `patch_page`, `list_pages`, `find_pages`, `get_page`, `list_organizations`) need a token. Credentials resolve in order: **per-request header → env var → saved `auth.json`** (`login`).
|
|
92
|
+
The **reference + generation tools** (`get_generation_guide`, `list_elements`, `validate_page`, …) and the **ingest tools** (`ingest_html`, `ingest_url` — turn an existing HTML or URL into a layout anchor so the AI can recreate or adapt it) work with **zero config**; only the **persistence tools** (`create_page`, `update_page`, `add_section`, `patch_page`, `publish_page`, `list_pages`, `find_pages`, `get_page`, `list_organizations`) need a token. Credentials resolve in order: **per-request header → env var → saved `auth.json`** (`login`).
|
|
93
93
|
|
|
94
94
|
> 🛠️ Prefer a shell-script installer (`install.sh`/`install.ps1`), a cloned local build, or hand-written per-IDE config? See **[docs/manual-install.md](docs/manual-install.md)**.
|
|
95
95
|
|
|
@@ -222,7 +222,8 @@ lands in logs). Any header that's missing falls back to the matching env var:
|
|
|
222
222
|
| `x-webcake-org-id` | `WEBCAKE_ORG_ID` | default org |
|
|
223
223
|
| `x-webcake-api-base` | `WEBCAKE_API_BASE` | overrides the env preset's API base |
|
|
224
224
|
| `x-webcake-app-base` | `WEBCAKE_APP_BASE` | overrides the env preset's SPA base (login connect page) |
|
|
225
|
-
| `x-webcake-builder-base` | `WEBCAKE_BUILDER_BASE` | overrides the builder host used for editor
|
|
225
|
+
| `x-webcake-builder-base` | `WEBCAKE_BUILDER_BASE` | overrides the builder host used for editor links |
|
|
226
|
+
| `x-webcake-preview-base` | `WEBCAKE_PREVIEW_BASE` | overrides the public preview host used for `/preview/<id>` links |
|
|
226
227
|
|
|
227
228
|
> The reference + generation tools (`get_generation_guide`, `list_elements`, `validate_page`, …) need **no
|
|
228
229
|
> token** — only the persistence tools (`create_page`, `update_page`, …) use it. Without a JWT, those return
|
|
@@ -292,7 +293,8 @@ flow can also be done entirely in the SPA, no backend route needed.)
|
|
|
292
293
|
| `WEBCAKE_JWT` | No* | Account JWT (dashboard auth). Required to persist — expires, refresh when needed. |
|
|
293
294
|
| `WEBCAKE_ORG_ID` | No | Default organization id for `create_page` (overridden by its `organization_id` arg). Omit → personal page. |
|
|
294
295
|
| `WEBCAKE_APP_BASE` | No | Optional SPA base — used for the browser `login` connect page. |
|
|
295
|
-
| `WEBCAKE_BUILDER_BASE` | No | Optional builder host for the editor
|
|
296
|
+
| `WEBCAKE_BUILDER_BASE` | No | Optional builder host for the editor links in the result. Defaults to the env preset, else derived from the API host (`api.x`→`builder.x`). |
|
|
297
|
+
| `WEBCAKE_PREVIEW_BASE` | No | Optional public preview host for the `/preview/<id>` links — NOT the builder subdomain. Defaults to the env preset (`preview.localhost:5800` local / `staging.webcake.me` staging / `www.webcake.me` prod). |
|
|
296
298
|
| `WEBCAKE_CONFIG_DIR` | No | Dir for the saved `auth.json` written by `login` (default `~/.webcake-landing-mcp`). |
|
|
297
299
|
|
|
298
300
|
> \* `WEBCAKE_API_BASE` and `WEBCAKE_JWT` are only needed for the persistence tools. The reference and
|
|
@@ -419,6 +421,9 @@ update_page({ page_id, source, dry_run: false }) # overwrite (dry_run=tr
|
|
|
419
421
|
create_page({ source: smallSkeleton, dry_run: false }) # → page_id
|
|
420
422
|
add_section({ page_id, sections: heroSection, dry_run: false }) # backend appends server-side (no whole-source get+put)
|
|
421
423
|
add_section({ page_id, sections: [formSection, footerSection], dry_run: false })
|
|
424
|
+
|
|
425
|
+
# Go LIVE (the preview link works without this — publish to attach a domain / set live status)
|
|
426
|
+
publish_page({ page_id, custom_domain: "shop.example.com", custom_path: "sale", dry_run: false })
|
|
422
427
|
```
|
|
423
428
|
|
|
424
429
|
`create_page` calls **`POST {WEBCAKE_API_BASE}/api/v1/ai/create_page_from_source`** on the backend.
|
|
@@ -467,6 +472,7 @@ Both `create_page` and `update_page` **default to `dry_run=true`** (validate and
|
|
|
467
472
|
| `find_pages` | Search the account's pages by name, domain, and/or page id (AND-combined) to locate one to edit; returns id, name, org, custom/default domain, updated_at. |
|
|
468
473
|
| `get_page` | Fetch an existing page's decoded source tree, COMPACTED to the sparse authoring shape (factory-default boilerplate stripped — far fewer tokens; `compact:false` for the raw tree). Edit and send back as-is. |
|
|
469
474
|
| `update_page` | Overwrite an existing page's source with an edited tree. **Defaults to `dry_run=true`.** |
|
|
475
|
+
| `publish_page` | Publish a page (live status, optional custom domain/path). The preview link works WITHOUT publishing — publish only to go live. **Defaults to `dry_run=true`.** |
|
|
470
476
|
|
|
471
477
|
---
|
|
472
478
|
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.50",
|
|
4
|
+
"d": "10/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "New publish_page tool makes a page live: reads the page's current stored source, saves it as a new version, and creates or updates the…",
|
|
7
|
+
"vi": "Công cụ publish_page mới giúp đưa trang lên live: đọc source đang lưu của trang, lưu thành phiên bản mới và tạo hoặc cập nhật bản ghi…"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.0.49",
|
|
4
11
|
"d": "10/06/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Changed",
|
|
34
41
|
"en": "get_generation_guide workflow condensed to four steps: element-type reads and image fetches are now batched into single calls…",
|
|
35
42
|
"vi": "Workflow trong get_generation_guide được rút gọn xuống còn bốn bước: việc đọc loại phần tử và tìm ảnh nay được gộp thành các lần gọi batch duy nhất…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.44",
|
|
39
|
-
"d": "09/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "New find_pages tool searches the account's pages by name, domain (matches custom_domain or default_domain), and/or page id (filters are…",
|
|
42
|
-
"vi": "Công cụ find_pages mới tìm kiếm các trang trong tài khoản theo tên, domain (khớp với custom_domain hoặc default_domain), và/hoặc page id (các bộ lọc…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -32,4 +32,6 @@ MODEL (essentials):
|
|
|
32
32
|
- Visible content lives in specials (text, src, field_name…), never in styles. Colors as rgba(). Animation in config.animation={name,delay,duration,repeat}. Form inputs need a unique specials.field_name (use canonical keys: full_name, phone_number, email, address, quantity).
|
|
33
33
|
- IMAGES: include them (hero/product, feature icons, about photo). PREFER REAL PHOTOS — call search_images with a short English subject (e.g. 'fresh coffee cup') and put a returned URL (src.large for a hero/banner, src.medium for a card/thumb) into the image-block specials.src; it works out of the box (a shared proxy supplies images). Only if search_images returns ok:false, FALL BACK to a PLACEHOLDER sized to the box: https://placehold.co/<width>x<height>. (gallery.media = array of OBJECTS {type:'image',link:'<url>',linkVideo:'',typeVideo:'youtube',imageCompression:true} — NOT plain strings, the gallery reads item.link; video.specials.img = poster). NEVER leave src empty (renders blank). Ensure text contrasts with its section background.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
- 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 }).
|
|
36
|
+
|
|
37
|
+
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.`;
|
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
* WEBCAKE_JWT the account JWT (required to call the backend)
|
|
20
20
|
* WEBCAKE_ORG_ID optional default organization id for create_page
|
|
21
21
|
* WEBCAKE_APP_BASE optional SPA base (used for the login connect page)
|
|
22
|
-
* WEBCAKE_BUILDER_BASE optional builder host for editor
|
|
22
|
+
* WEBCAKE_BUILDER_BASE optional builder host for the editor URLs in the result
|
|
23
23
|
* (defaults to the env preset, else derived from the API host)
|
|
24
|
+
* WEBCAKE_PREVIEW_BASE optional public preview host for the /preview/<id> links —
|
|
25
|
+
* NOT the builder subdomain (defaults to the env preset:
|
|
26
|
+
* preview.localhost:5800 / staging.webcake.me / www.webcake.me)
|
|
24
27
|
* WEBCAKE_CONFIG_DIR optional dir for the saved auth.json (default ~/.webcake-landing-mcp)
|
|
25
28
|
*/
|
|
26
29
|
import { homedir } from "node:os";
|
|
@@ -36,9 +39,9 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
36
39
|
* after create/update (a distinct host — NOT the API and NOT the SPA).
|
|
37
40
|
*/
|
|
38
41
|
export const ENVIRONMENTS = {
|
|
39
|
-
local: { apiBase: "http://localhost:5800", appBase: "http://localhost:5173", builderBase: "http://builder.localhost:5800" },
|
|
40
|
-
staging: { apiBase: "https://api.staging.webcake.io", appBase: "https://staging.webcake.io", builderBase: "https://builder.staging.webcake.io" },
|
|
41
|
-
prod: { apiBase: "https://api.webcake.io", appBase: "https://webcake.io", builderBase: "https://builder.webcake.io" },
|
|
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
45
|
};
|
|
43
46
|
export const ENV_NAMES = Object.keys(ENVIRONMENTS);
|
|
44
47
|
/** True when `v` names a known environment (local|staging|prod). */
|
|
@@ -88,6 +91,16 @@ export function readConfig(overrides = {}) {
|
|
|
88
91
|
preset?.builderBase ??
|
|
89
92
|
saved.builderBase ??
|
|
90
93
|
deriveBuilderBase(cleanBase))?.replace(/\/+$/, "");
|
|
94
|
+
// The public preview link (/preview/<id>) is served on its OWN root host — NOT
|
|
95
|
+
// the builder subdomain (preview.localhost:5800 / staging.webcake.me /
|
|
96
|
+
// www.webcake.me). When nothing matches, default to the backend's own preview
|
|
97
|
+
// domain (its @preview_domain) so the link still lands on a host that serves
|
|
98
|
+
// the /preview/:id route.
|
|
99
|
+
const previewBase = (overrides.previewBase ??
|
|
100
|
+
process.env.WEBCAKE_PREVIEW_BASE ??
|
|
101
|
+
preset?.previewBase ??
|
|
102
|
+
saved.previewBase ??
|
|
103
|
+
"https://www.webcake.me").replace(/\/+$/, "");
|
|
91
104
|
return {
|
|
92
105
|
config: {
|
|
93
106
|
base: cleanBase,
|
|
@@ -95,6 +108,7 @@ export function readConfig(overrides = {}) {
|
|
|
95
108
|
orgId: overrides.orgId ?? process.env.WEBCAKE_ORG_ID ?? saved.orgId,
|
|
96
109
|
appBase: (overrides.appBase ?? process.env.WEBCAKE_APP_BASE ?? preset?.appBase ?? saved.appBase)?.replace(/\/+$/, ""),
|
|
97
110
|
builderBase,
|
|
111
|
+
previewBase,
|
|
98
112
|
},
|
|
99
113
|
missing: [],
|
|
100
114
|
};
|
|
@@ -111,7 +125,8 @@ function header(headers, name) {
|
|
|
111
125
|
* x-webcake-env named environment (local|staging|prod) for the base URLs
|
|
112
126
|
* x-webcake-api-base backend base URL (overrides the env preset)
|
|
113
127
|
* x-webcake-app-base SPA base used for the login connect page (overrides the preset)
|
|
114
|
-
* x-webcake-builder-base builder host for editor
|
|
128
|
+
* x-webcake-builder-base builder host for editor URLs (overrides the preset)
|
|
129
|
+
* x-webcake-preview-base public preview host for /preview/<id> links (overrides the preset)
|
|
115
130
|
* Any header that is absent falls back to the corresponding env var in readConfig.
|
|
116
131
|
*/
|
|
117
132
|
export function configFromHeaders(headers) {
|
|
@@ -123,6 +138,7 @@ export function configFromHeaders(headers) {
|
|
|
123
138
|
orgId: header(headers, "x-webcake-org-id"),
|
|
124
139
|
appBase: header(headers, "x-webcake-app-base"),
|
|
125
140
|
builderBase: header(headers, "x-webcake-builder-base"),
|
|
141
|
+
previewBase: header(headers, "x-webcake-preview-base"),
|
|
126
142
|
env: header(headers, "x-webcake-env"),
|
|
127
143
|
};
|
|
128
144
|
}
|
|
@@ -5,6 +5,13 @@ const SEARCH_PAGES_ENDPOINT = "/api/v1/ai/search_pages";
|
|
|
5
5
|
const PAGE_SOURCE_ENDPOINT = "/api/v1/ai/page_source";
|
|
6
6
|
const UPDATE_ENDPOINT = "/api/v1/ai/update_page_source";
|
|
7
7
|
const APPEND_ENDPOINT = "/api/v1/ai/append_section";
|
|
8
|
+
// The editor's own publish route (NOT under /api/v1/ai): saves the source as a
|
|
9
|
+
// new version and creates/updates the page_published record (+ optional custom
|
|
10
|
+
// domain/path) so the page goes live. NOTE: this scope is host-constrained to
|
|
11
|
+
// the BUILDER host (router scope `host: "builder."`), so the request goes to
|
|
12
|
+
// config.builderBase, not the API base.
|
|
13
|
+
const publishEndpoint = (pageId) => `/api/pages/${encodeURIComponent(pageId)}/edit/publish`;
|
|
14
|
+
const publishUrl = (config, pageId) => `${(config.builderBase ?? config.base).replace(/\/+$/, "")}${publishEndpoint(pageId)}`;
|
|
8
15
|
function authHeaders(config, orgId) {
|
|
9
16
|
const headers = {
|
|
10
17
|
"Content-Type": "application/json",
|
|
@@ -44,6 +51,33 @@ export function toEditorUrl(config, raw) {
|
|
|
44
51
|
pathQuery = `/${pathQuery}`;
|
|
45
52
|
return `${builder.replace(/\/+$/, "")}${pathQuery}`;
|
|
46
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the public preview link (`/preview/<page_id>`) onto the PREVIEW host
|
|
56
|
+
* (config.previewBase) — NOT the builder subdomain. The /preview/:id route only
|
|
57
|
+
* exists on the root preview hosts (preview.localhost:5800 local /
|
|
58
|
+
* staging.webcake.me staging / www.webcake.me prod); the v4 renderer there reads
|
|
59
|
+
* the stored page_source directly, so the link works without publishing.
|
|
60
|
+
*/
|
|
61
|
+
export function toPreviewUrl(config, raw) {
|
|
62
|
+
if (!raw)
|
|
63
|
+
return raw;
|
|
64
|
+
const preview = config.previewBase;
|
|
65
|
+
if (!preview)
|
|
66
|
+
return toEditorUrl(config, raw); // legacy fallback
|
|
67
|
+
let pathQuery = raw;
|
|
68
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
69
|
+
try {
|
|
70
|
+
const u = new URL(raw);
|
|
71
|
+
pathQuery = u.pathname + u.search + u.hash;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* not a parseable URL — use as-is */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!pathQuery.startsWith("/"))
|
|
78
|
+
pathQuery = `/${pathQuery}`;
|
|
79
|
+
return `${preview.replace(/\/+$/, "")}${pathQuery}`;
|
|
80
|
+
}
|
|
47
81
|
/** Build (but do not send) the create request — used for dry-run previews. */
|
|
48
82
|
export function buildRequest(config, name, source, orgId) {
|
|
49
83
|
return {
|
|
@@ -134,7 +168,7 @@ export async function createPage(config, name, source, orgId) {
|
|
|
134
168
|
status: res.status,
|
|
135
169
|
page_id: pageId,
|
|
136
170
|
editor_url: toEditorUrl(config, editorPath),
|
|
137
|
-
preview_url:
|
|
171
|
+
preview_url: toPreviewUrl(config, previewPath),
|
|
138
172
|
organization_id: (orgId ?? config.orgId) ?? null,
|
|
139
173
|
raw: data,
|
|
140
174
|
};
|
|
@@ -278,7 +312,7 @@ export async function appendSection(config, pageId, sections) {
|
|
|
278
312
|
status: res.status,
|
|
279
313
|
page_id: pageIdOut,
|
|
280
314
|
editor_url: toEditorUrl(config, data?.editor_url),
|
|
281
|
-
preview_url:
|
|
315
|
+
preview_url: toPreviewUrl(config, data?.preview_url),
|
|
282
316
|
organization_id: data?.organization_id ?? null,
|
|
283
317
|
section_count: data?.section_count,
|
|
284
318
|
sections_added: data?.sections_added,
|
|
@@ -323,8 +357,111 @@ export async function updatePageSource(config, pageId, source) {
|
|
|
323
357
|
status: res.status,
|
|
324
358
|
page_id: pageIdOut,
|
|
325
359
|
editor_url: toEditorUrl(config, data?.editor_url),
|
|
326
|
-
preview_url:
|
|
360
|
+
preview_url: toPreviewUrl(config, data?.preview_url),
|
|
327
361
|
organization_id: data?.organization_id ?? null,
|
|
328
362
|
raw: data,
|
|
329
363
|
};
|
|
330
364
|
}
|
|
365
|
+
function publishBody(sourceString, opts = {}) {
|
|
366
|
+
// The publish action expects `source` as a JSON STRING (it Jason.decode!s it),
|
|
367
|
+
// plus optional custom_domain/custom_path. is_publish marks the save as a
|
|
368
|
+
// publish in save_page_with_source.
|
|
369
|
+
return JSON.stringify({
|
|
370
|
+
source: sourceString,
|
|
371
|
+
custom_domain: opts.customDomain ?? "",
|
|
372
|
+
custom_path: opts.customPath ?? "",
|
|
373
|
+
is_publish: true,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/** Build (but do not send) the publish request with the token masked — for dry-run previews. */
|
|
377
|
+
export function buildPublishRequestRedacted(config, pageId, sourceString, opts = {}) {
|
|
378
|
+
const body = publishBody(sourceString, opts);
|
|
379
|
+
return {
|
|
380
|
+
method: "POST",
|
|
381
|
+
url: publishUrl(config, pageId),
|
|
382
|
+
headers: { ...authHeaders(config), Authorization: "Bearer ***JWT***", Cookie: "jwt=***JWT***" },
|
|
383
|
+
body: body.replace(config.jwt, "***JWT***").slice(0, 400) + (body.length > 400 ? `… (${body.length} bytes)` : ""),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* POST to a host-scoped route. Node's fetch cannot reach `*.localhost` hosts
|
|
388
|
+
* (browsers special-case .localhost; Node's DNS does not, and undici forbids a
|
|
389
|
+
* manual Host header) — so for those we connect to loopback via node:http and
|
|
390
|
+
* carry the real host in the Host header. Everything else uses plain fetch.
|
|
391
|
+
*/
|
|
392
|
+
async function postToHost(url, headers, body) {
|
|
393
|
+
const u = new URL(url);
|
|
394
|
+
if (!u.hostname.endsWith(".localhost")) {
|
|
395
|
+
const res = await fetch(url, { method: "POST", headers, body });
|
|
396
|
+
return { status: res.status, text: await res.text() };
|
|
397
|
+
}
|
|
398
|
+
const { request } = await import("node:http");
|
|
399
|
+
return new Promise((resolve, reject) => {
|
|
400
|
+
const req = request({
|
|
401
|
+
host: "127.0.0.1",
|
|
402
|
+
port: u.port || 80,
|
|
403
|
+
path: u.pathname + u.search,
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: { ...headers, Host: u.host },
|
|
406
|
+
}, (res) => {
|
|
407
|
+
let data = "";
|
|
408
|
+
res.on("data", (c) => (data += c));
|
|
409
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, text: data }));
|
|
410
|
+
});
|
|
411
|
+
req.on("error", reject);
|
|
412
|
+
req.end(body);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Publish a page: saves the source as a new version and creates/updates the
|
|
417
|
+
* page_published record (live status + optional custom domain/path). Returns the
|
|
418
|
+
* resulting public URL — `https://<domain>/<path>` when a custom domain is
|
|
419
|
+
* attached, else the preview-host link (`<previewBase>/preview/<page_id>`).
|
|
420
|
+
*/
|
|
421
|
+
export async function publishPage(config, pageId, sourceString, opts = {}) {
|
|
422
|
+
const url = publishUrl(config, pageId);
|
|
423
|
+
let status;
|
|
424
|
+
let text;
|
|
425
|
+
try {
|
|
426
|
+
// The builder-host pipeline runs an `accepts ["html"]` plug (it serves the
|
|
427
|
+
// editor SPA); a literal application/json Accept gets a 406, so send */*
|
|
428
|
+
// like the browser does — the action still returns JSON.
|
|
429
|
+
({ status, text } = await postToHost(url, { ...authHeaders(config), Accept: "*/*" }, publishBody(sourceString, opts)));
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
return { ok: false, status: 0, error: `Network error calling ${url}: ${e?.message ?? e}` };
|
|
433
|
+
}
|
|
434
|
+
let json = null;
|
|
435
|
+
try {
|
|
436
|
+
json = JSON.parse(text);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
/* non-JSON */
|
|
440
|
+
}
|
|
441
|
+
const resOk = status >= 200 && status < 300;
|
|
442
|
+
const success = json?.success !== false && resOk;
|
|
443
|
+
if (!success) {
|
|
444
|
+
const backendMsg = json?.message ?? json?.reason ?? (json ? undefined : text.slice(0, 200));
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
status,
|
|
448
|
+
raw: json ?? text.slice(0, 600),
|
|
449
|
+
error: `Backend returned ${status}${backendMsg ? `: ${backendMsg}` : ""}`,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const data = json?.data ?? json;
|
|
453
|
+
const domain = data?.domain ?? null;
|
|
454
|
+
const path = data?.path ?? null;
|
|
455
|
+
const previewUrl = toPreviewUrl(config, `/preview/${pageId}`);
|
|
456
|
+
const publishedUrl = domain ? `https://${domain}${path ? `/${String(path).replace(/^\/+/, "")}` : ""}` : previewUrl;
|
|
457
|
+
return {
|
|
458
|
+
ok: true,
|
|
459
|
+
status,
|
|
460
|
+
page_id: pageId,
|
|
461
|
+
published_url: publishedUrl,
|
|
462
|
+
preview_url: previewUrl,
|
|
463
|
+
domain,
|
|
464
|
+
path,
|
|
465
|
+
raw: data,
|
|
466
|
+
};
|
|
467
|
+
}
|
package/dist/smoke.js
CHANGED
|
@@ -7,8 +7,8 @@ import { validatePage, pageSchema } from "./domains/landing/validate.js";
|
|
|
7
7
|
import { expandSource } from "./core/expand.js";
|
|
8
8
|
import { compactSource, deepEq, sparseTemplate } from "./core/compact.js";
|
|
9
9
|
import { parseHtml } from "./persistence/html-ingest.js";
|
|
10
|
-
import { readConfig, resolveEnv, ENV_NAMES } from "./persistence/config.js";
|
|
11
|
-
import { toEditorUrl } from "./persistence/webcake-client.js";
|
|
10
|
+
import { readConfig, resolveEnv, ENV_NAMES, configFromHeaders } from "./persistence/config.js";
|
|
11
|
+
import { toEditorUrl, toPreviewUrl, buildPublishRequestRedacted } from "./persistence/webcake-client.js";
|
|
12
12
|
import { normalizePhoto, resolvePexelsKey, pexelsKeyFromHeaders, resolvePexelsProxyBase, buildSearchQuery, PEXELS_PROXY_DEFAULT } from "./persistence/pexels-client.js";
|
|
13
13
|
let failures = 0;
|
|
14
14
|
const check = (name, cond, extra) => {
|
|
@@ -327,7 +327,7 @@ check("clean form has no binding warnings", rbg.warnings.length === 0, rbg.warni
|
|
|
327
327
|
console.log("== config: named environment presets (local/staging/prod) ==");
|
|
328
328
|
{
|
|
329
329
|
// Deterministic: isolate from any ambient WEBCAKE_* and the saved auth.json on the dev box.
|
|
330
|
-
for (const k of ["WEBCAKE_API_BASE", "WEBCAKE_APP_BASE", "WEBCAKE_BUILDER_BASE", "WEBCAKE_ENV", "WEBCAKE_JWT", "WEBCAKE_ORG_ID"])
|
|
330
|
+
for (const k of ["WEBCAKE_API_BASE", "WEBCAKE_APP_BASE", "WEBCAKE_BUILDER_BASE", "WEBCAKE_PREVIEW_BASE", "WEBCAKE_ENV", "WEBCAKE_JWT", "WEBCAKE_ORG_ID"])
|
|
331
331
|
delete process.env[k];
|
|
332
332
|
process.env.WEBCAKE_CONFIG_DIR = "/nonexistent/webcake-smoke";
|
|
333
333
|
check("env names are local/staging/prod", setEq(new Set(ENV_NAMES), ["local", "staging", "prod"]), ENV_NAMES);
|
|
@@ -353,6 +353,23 @@ console.log("== config: named environment presets (local/staging/prod) ==");
|
|
|
353
353
|
check("editor url from a path → builder host", toEditorUrl(localCfg, "/editor/v2/abc") === "http://builder.localhost:5800/editor/v2/abc");
|
|
354
354
|
check("editor url from an absolute api url → builder host", toEditorUrl(localCfg, "http://localhost:5800/editor/v2/abc?x=1") === "http://builder.localhost:5800/editor/v2/abc?x=1");
|
|
355
355
|
check("editor url passthrough when empty", toEditorUrl(localCfg, undefined) === undefined);
|
|
356
|
+
// The PREVIEW link lives on its own root host (NOT the builder subdomain):
|
|
357
|
+
// preview.localhost:5800 / staging.webcake.me / www.webcake.me.
|
|
358
|
+
check("env presets carry preview bases", resolveEnv("local")?.previewBase === "http://preview.localhost:5800" && resolveEnv("staging")?.previewBase === "https://staging.webcake.me" && resolveEnv("prod")?.previewBase === "https://www.webcake.me");
|
|
359
|
+
check("readConfig(env=local) sets previewBase", localCfg.previewBase === "http://preview.localhost:5800", localCfg);
|
|
360
|
+
check("readConfig(env=prod) sets previewBase", readConfig({ env: "prod", jwt: "t" }).config?.previewBase === "https://www.webcake.me");
|
|
361
|
+
check("previewBase defaults to www.webcake.me without a preset", readConfig({ base: "https://api.example.com", jwt: "t" }).config?.previewBase === "https://www.webcake.me");
|
|
362
|
+
check("explicit previewBase overrides the preset", readConfig({ env: "prod", previewBase: "https://p.test/", jwt: "t" }).config?.previewBase === "https://p.test");
|
|
363
|
+
check("x-webcake-preview-base header parsed", configFromHeaders({ "x-webcake-preview-base": "https://p.example" }).previewBase === "https://p.example");
|
|
364
|
+
check("preview url from a path → preview host (not builder)", toPreviewUrl(localCfg, "/preview/abc") === "http://preview.localhost:5800/preview/abc");
|
|
365
|
+
check("preview url from an absolute api url → preview host", toPreviewUrl(localCfg, "http://localhost:5800/preview/abc?x=1") === "http://preview.localhost:5800/preview/abc?x=1");
|
|
366
|
+
check("preview url passthrough when empty", toPreviewUrl(localCfg, undefined) === undefined);
|
|
367
|
+
check("preview url falls back to builder when previewBase missing", toPreviewUrl({ ...localCfg, previewBase: undefined }, "/preview/abc") === "http://builder.localhost:5800/preview/abc");
|
|
368
|
+
// publish request preview: JWT must be masked everywhere.
|
|
369
|
+
const pub = buildPublishRequestRedacted({ ...localCfg, jwt: "SECRETJWT" }, "pg1", JSON.stringify({ page: [] }), { customDomain: "shop.example.com", customPath: "sale" });
|
|
370
|
+
check("publish request hits the editor publish route on the BUILDER host", pub.url === "http://builder.localhost:5800/api/pages/pg1/edit/publish", pub.url);
|
|
371
|
+
check("publish request masks the JWT", !JSON.stringify(pub).includes("SECRETJWT"), pub);
|
|
372
|
+
check("publish request carries domain/path + source string", pub.body.includes("shop.example.com") && pub.body.includes("custom_path") && pub.body.includes("is_publish"), pub.body);
|
|
356
373
|
}
|
|
357
374
|
console.log("== pexels: key resolution + photo normalization (offline, no network) ==");
|
|
358
375
|
{
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { text } from "../mcp/response.js";
|
|
14
14
|
import { readConfig, configFromHeaders } from "../persistence/config.js";
|
|
15
|
-
import { buildRequestRedacted, buildUpdateRequestRedacted, buildAppendRequestRedacted, createPage, listOrganizations, listPages, searchPages, getPageSource, updatePageSource, appendSection, } from "../persistence/webcake-client.js";
|
|
15
|
+
import { buildRequestRedacted, buildUpdateRequestRedacted, buildAppendRequestRedacted, buildPublishRequestRedacted, createPage, listOrganizations, listPages, searchPages, getPageSource, updatePageSource, appendSection, publishPage, toPreviewUrl, } from "../persistence/webcake-client.js";
|
|
16
16
|
import { putDraft, getDraft, updateDraft, deleteDraft } from "../persistence/draft-cache.js";
|
|
17
17
|
export function registerPersistenceTools(server, domain) {
|
|
18
18
|
// Resolve config from THIS request's headers (remote per-user JWT) first, then env.
|
|
@@ -691,4 +691,51 @@ export function registerPersistenceTools(server, domain) {
|
|
|
691
691
|
warnings: result.warnings,
|
|
692
692
|
});
|
|
693
693
|
});
|
|
694
|
+
// 15) Publish page (go live) -------------------------------------------------
|
|
695
|
+
server.tool("publish_page", "Publishes an EXISTING page: saves the stored source as a new version and creates/updates its page_published record (live status), optionally attaching a custom domain/path. NOT needed for the preview link — /preview/<page_id> on the preview host renders the stored source directly; publish when the user wants the page LIVE (custom domain, or the public published URL). Note: this publishes source-only (no editor-rendered HTML); pages last published from the editor with custom head/body should be re-published there. DEFAULTS to dry_run=true. Needs WEBCAKE_API_BASE + WEBCAKE_JWT.", {
|
|
696
|
+
page_id: z.string().describe("The page id to publish (must be owned by the account)."),
|
|
697
|
+
custom_domain: z
|
|
698
|
+
.string()
|
|
699
|
+
.optional()
|
|
700
|
+
.describe("Optional custom domain to serve the page at (e.g. 'shop.example.com' — must already point at Webcake). Omit to publish without a domain (served at the preview-host URL)."),
|
|
701
|
+
custom_path: z.string().optional().describe("Optional path under the custom domain (e.g. 'sale')."),
|
|
702
|
+
dry_run: z.boolean().optional().describe("Default TRUE — preview the request without sending. Set false to actually publish."),
|
|
703
|
+
}, { title: "Publish Webcake Page", readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async ({ page_id, custom_domain, custom_path, dry_run }, extra) => {
|
|
704
|
+
const isDry = dry_run !== false; // default true (safe)
|
|
705
|
+
const { config, missing } = cfgFor(extra);
|
|
706
|
+
if (!config)
|
|
707
|
+
return text({ published: false, reason: "missing_env", missing_env: missing });
|
|
708
|
+
// Publish re-saves the page's CURRENT stored source (the publish endpoint
|
|
709
|
+
// requires the source in the request), so read it first — even on dry_run,
|
|
710
|
+
// to show the real payload.
|
|
711
|
+
const res = await getPageSource(config, page_id);
|
|
712
|
+
if (!res.ok || res.source == null) {
|
|
713
|
+
return text({ published: false, reason: "page_not_found", status: res.status, error: res.error ?? "No source on this page." });
|
|
714
|
+
}
|
|
715
|
+
const sourceString = JSON.stringify(res.source);
|
|
716
|
+
const opts = { customDomain: custom_domain, customPath: custom_path };
|
|
717
|
+
if (isDry) {
|
|
718
|
+
return text({
|
|
719
|
+
dry_run: true,
|
|
720
|
+
page_id,
|
|
721
|
+
name: res.name,
|
|
722
|
+
would_publish_to: custom_domain
|
|
723
|
+
? `https://${custom_domain}${custom_path ? `/${custom_path}` : ""}`
|
|
724
|
+
: toPreviewUrl(config, `/preview/${page_id}`),
|
|
725
|
+
request: buildPublishRequestRedacted(config, page_id, sourceString, opts),
|
|
726
|
+
hint: "Re-run with dry_run=false to actually publish.",
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
const outcome = await publishPage(config, page_id, sourceString, opts);
|
|
730
|
+
return text({
|
|
731
|
+
published: outcome.ok,
|
|
732
|
+
page_id,
|
|
733
|
+
url: outcome.published_url,
|
|
734
|
+
preview_url: outcome.preview_url,
|
|
735
|
+
domain: outcome.domain,
|
|
736
|
+
path: outcome.path,
|
|
737
|
+
status: outcome.status,
|
|
738
|
+
error: outcome.error,
|
|
739
|
+
});
|
|
740
|
+
});
|
|
694
741
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.50",
|
|
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",
|