webcake-landing-mcp 1.0.57 → 1.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.59",
|
|
4
|
+
"d": "11/06/2026",
|
|
5
|
+
"type": "Changed",
|
|
6
|
+
"en": "create_page now resolves the organization automatically on the real run (dry_run:false): if the account has exactly one org it is auto-selected and…",
|
|
7
|
+
"vi": "create_page nay tự phân giải tổ chức trên lần chạy thực (dry_run:false): nếu tài khoản có đúng một org thì tự động chọn và kết quả có thêm…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.0.58",
|
|
11
|
+
"d": "11/06/2026",
|
|
12
|
+
"type": "Changed",
|
|
13
|
+
"en": "html-box descriptor (get_element) has been rewritten to document COMPOSITE VISUALS as the primary use case: intricate non-interactive mockups such…",
|
|
14
|
+
"vi": "Descriptor html-box (get_element) được viết lại để ghi lại COMPOSITE VISUALS là trường hợp sử dụng chính: các mockup phi tương tác phức tạp như…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.0.57",
|
|
4
18
|
"d": "11/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Added",
|
|
27
41
|
"en": "The installer (install command) now supports five additional IDE/agent targets: Antigravity, Gemini CLI, Cline, Kiro, and OpenCode; pass --ide…",
|
|
28
42
|
"vi": "Trình cài đặt (lệnh install) nay hỗ trợ thêm năm IDE/agent: Antigravity, Gemini CLI, Cline, Kiro và OpenCode; truyền --ide antigravity, --ide…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.0.53",
|
|
32
|
-
"d": "10/06/2026",
|
|
33
|
-
"type": "Fixed",
|
|
34
|
-
"en": "The login command on Windows now opens the connect URL correctly: cmd /c start previously split the URL at the first & character (treating it as a…",
|
|
35
|
-
"vi": "Lệnh login trên Windows nay mở URL kết nối chính xác: trước đây cmd /c start tách URL tại ký tự & đầu tiên (do hiểu & là dấu phân cách lệnh), khiến…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.52",
|
|
39
|
-
"d": "10/06/2026",
|
|
40
|
-
"type": "Added",
|
|
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,…",
|
|
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,…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -237,13 +237,25 @@ export const CONTENT = [
|
|
|
237
237
|
{
|
|
238
238
|
type: "html-box", category: "content", container: false, defaultName: "HTML Box",
|
|
239
239
|
summary: "Raw HTML embed. specials.html holds the markup stored HTML-escaped (unescaped at render via v-html). Contrast with editor-blog which stores html RAW. Embedded <iframe> is auto-stretched to 100%×100% of the box. Wrapper height is FIXED to styles.height — content taller than the box overflows. in the stored value becomes a space at render. Use unescape-safe HTML (e.g. '<' → the literal character '<' after unescape).",
|
|
240
|
-
useWhen: "
|
|
240
|
+
useWhen: "COMPOSITE VISUALS (primary use): intricate, non-interactive mockups with 10+ nested parts — phone/chat mockup (message thread with bubbles/avatars/tags), mini dashboard, browser-window frame, inbox/notification list, ticket-style card. Use ONE html-box per mockup instead of 30-40 absolute-positioned elements. Recipe: (1) inline styles ONLY — no class/id (no stylesheet inside the box); (2) root div: style='width:100%;height:100%;box-sizing:border-box;overflow:hidden;…' — fills the element box; flex/grid are fine INSIDE the box (absolute-canvas rule applies only to Webcake elements); (3) design inner content to fit styles.height — content taller overflows silently; (4) specials.html must be HTML-ESCAPED ('<div…'); (5) set BOTH breakpoints with real width/height; (6) set font-family inline on the root if the mockup needs a specific font. WHEN NOT to use: real headings, CTAs, forms, prices, any content the user/editor must edit per-piece, or any element that events must target — html-box is an opaque blob in the editor and invisible to form/event wiring; never put the page's primary copy in one. ALSO: embedding third-party widgets or custom markup the standard elements can't express.",
|
|
241
241
|
keySpecials: { html: "string — raw HTML content stored HTML-escaped (e.g. '<p>Hello</p>'); the renderer unescapes it before injecting via v-html. The wrapper height is FIXED to styles.height — content that is taller overflows the box. An embedded <iframe> is auto-stretched to 100%×100% of the box." },
|
|
242
242
|
seed: (el) => {
|
|
243
243
|
seedPosition(el);
|
|
244
244
|
setBox(el, 280, 310);
|
|
245
245
|
el.specials.html = "";
|
|
246
246
|
},
|
|
247
|
+
example: {
|
|
248
|
+
id: "phone_chat1", type: "html-box",
|
|
249
|
+
responsive: {
|
|
250
|
+
desktop: { styles: { top: 60, left: 330, width: 300, height: 360, position: "absolute" } },
|
|
251
|
+
mobile: { styles: { top: 60, left: 60, width: 300, height: 360, position: "absolute" } },
|
|
252
|
+
},
|
|
253
|
+
specials: {
|
|
254
|
+
// Phone-chat mockup: rounded shell + header bar + 2 chat bubbles.
|
|
255
|
+
// Stored HTML-ESCAPED — the renderer unescapes before v-html injection.
|
|
256
|
+
html: "<div style="width:100%;height:100%;box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;border-radius:24px;background:#fff;box-shadow:0 8px 32px rgba(0,0,0,0.18);font-family:system-ui,sans-serif;"><div style="background:#4f46e5;padding:14px 16px;display:flex;align-items:center;gap:10px;"><div style="width:36px;height:36px;border-radius:50%;background:#a5b4fc;display:flex;align-items:center;justify-content:center;font-size:16px;color:#fff;font-weight:bold;">W</div><div><div style="color:#fff;font-weight:600;font-size:14px;">Webcake Support</div><div style="color:#c7d2fe;font-size:11px;">Online</div></div></div><div style="flex:1;padding:16px;display:flex;flex-direction:column;gap:12px;background:#f5f5f5;"><div style="display:flex;gap:8px;align-items:flex-end;"><div style="width:28px;height:28px;border-radius:50%;background:#4f46e5;flex-shrink:0;"></div><div style="background:#fff;border-radius:16px 16px 16px 4px;padding:10px 14px;font-size:13px;color:#1a1a2e;max-width:200px;box-shadow:0 1px 4px rgba(0,0,0,0.08);">Chào bạn! Mình có thể giúp gì cho bạn hôm nay? 😊</div></div><div style="display:flex;gap:8px;align-items:flex-end;flex-direction:row-reverse;"><div style="background:#4f46e5;border-radius:16px 16px 4px 16px;padding:10px 14px;font-size:13px;color:#fff;max-width:200px;">Mình muốn tạo landing page cho shop! 🚀</div></div></div></div>",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
247
259
|
},
|
|
248
260
|
{
|
|
249
261
|
type: "editor-blog", category: "content", container: false, defaultName: "Editor blog",
|
|
@@ -102,6 +102,7 @@ SECTION BUILD HINTS (apply to whichever sections the chosen archetype uses)
|
|
|
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
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.
|
|
105
|
+
- COMPLEX COMPOSITE VISUAL (phone/chat mockup, mini dashboard, browser-window frame, inbox/notification list, ticket-style card) → ONE html-box with inline-styled HTML (root div: width:100%;height:100%;box-sizing:border-box;overflow:hidden; flex/grid allowed inside; content must fit styles.height or it overflows silently) instead of dozens of tiny elements. Never use html-box for primary copy, CTAs, form fields, or any element that events must target — it is an opaque blob in the editor.
|
|
105
106
|
|
|
106
107
|
RULES
|
|
107
108
|
- Visible content goes in "specials" (text-block.specials.text, image-block.specials.src…), NEVER in "styles".
|
|
@@ -123,7 +124,7 @@ INTAKE — act as a DESIGN CONSULTANT, not a form. Goal: understand what the cus
|
|
|
123
124
|
- Primary CTA + where it goes: open a form popup, scroll to form, call/Zalo, open link?
|
|
124
125
|
- Form fields to capture (if any): name, phone, email, address, quantity…? (canonical field_names: full_name, phone_number, email, address, quantity).
|
|
125
126
|
- Look & feel: primary color (rgba/hex), logo/image URLs, must-keep text, things to avoid, references they like.
|
|
126
|
-
- Target: desktop+mobile or mobile-only? Which organization to save into (list_organizations)?
|
|
127
|
+
- Target: desktop+mobile or mobile-only? Which organization to save into — only ask when the account has 2+ organizations (call list_organizations to check; if there is exactly one org, save into it automatically; if the user explicitly wants no org, pass organization_id:"personal")?
|
|
127
128
|
CONSULT, don't interrogate — SUGGEST so the customer reacts to something concrete instead of inventing from scratch:
|
|
128
129
|
- ALWAYS offer sensible defaults with each question so they can just say "yes" (and answer fast).
|
|
129
130
|
- When the customer is vague, propose 2–3 concrete directions to choose from — e.g. an archetype/section flow, a hero treatment (image-beside / full-bg overlay / centered type), a color/tone direction — and let them pick or adjust.
|
|
@@ -141,7 +142,7 @@ WORKFLOW (recommended)
|
|
|
141
142
|
3b. For every image the page needs (hero, product, about, feature, gallery), call search_images and put a returned URL into specials.src / gallery item.link. Use placehold.co ONLY when search_images returns ok:false.
|
|
142
143
|
4. Assemble { page, popup, settings, options, cartConfigs }.
|
|
143
144
|
5. Call validate_page and fix every error.
|
|
144
|
-
6. To save: call list_organizations
|
|
145
|
+
6. To save: call list_organizations. If the account has EXACTLY ONE organization, create_page will auto-select it — no need to ask. If there are MULTIPLE organizations, show them to the user and ask which to use (highlight is_default as the suggested default); pass the chosen organization_id to create_page. If the user explicitly wants to save without any organization, pass organization_id:"personal". Then create_page (dry_run first, then dry_run:false). Note: create_page itself enforces this — it refuses to guess between multiple orgs and returns the org list asking you to pick.
|
|
145
146
|
|
|
146
147
|
EDITING an existing page
|
|
147
148
|
- find_pages / list_pages → let the user pick (or take a page_id from a URL).
|
|
@@ -153,5 +154,5 @@ EDITING an existing page
|
|
|
153
154
|
|
|
154
155
|
REFERENCE INPUT (HTML page to clone or adapt)
|
|
155
156
|
- 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
|
+
- 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 (clone its HTML with all styles inlined), NOT as element soup.
|
|
157
158
|
- 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.`;
|
|
@@ -23,8 +23,8 @@ RULES (follow for every request):
|
|
|
23
23
|
COMMIT-AS-IS (retry path after timeout): patch_page({ draft_id, dry_run:false }) with NO patches — skips the apply step, re-validates, then saves. This is the universal retry for any timed-out write.
|
|
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
|
-
- Organizations: call list_organizations and ask which to use
|
|
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.
|
|
26
|
+
- Organizations: call list_organizations before saving. If the account has exactly ONE org, create_page auto-selects it — no need to ask. If there are MULTIPLE orgs, show them and ask the user which to use (is_default is the suggested default); pass the chosen organization_id to create_page. Pass organization_id:"personal" only when the user explicitly wants no org. create_page enforces this itself (it refuses to guess between multiple orgs). 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 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). When the reference contains a composite widget (phone/device mockup, chat thread, mini dashboard, browser frame) → rebuild it as ONE html-box (clone its HTML with all styles inlined), not as element soup. (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.
|
|
@@ -31,7 +31,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
31
31
|
return text(await listOrganizations(config));
|
|
32
32
|
});
|
|
33
33
|
// 9) Create page (persist) --------------------------------------------------
|
|
34
|
-
server.tool("create_page", "Persists a page source to the configured Webcake backend: creates a NEW page and saves the source (source-only — opens in the editor where re-saving renders it). Validates first. DEFAULTS to dry_run=true (validates, caches the source as draft_id, returns the HTTP request it WOULD send, token masked); dry_run=false to actually create. Accepts draft_id from a previous call (validation failure, dry_run, or a timed-out create) — re-runs from the cached source without re-sending the full JSON.
|
|
34
|
+
server.tool("create_page", "Persists a page source to the configured Webcake backend: creates a NEW page and saves the source (source-only — opens in the editor where re-saving renders it). Validates first. DEFAULTS to dry_run=true (validates, caches the source as draft_id, returns the HTTP request it WOULD send, token masked); dry_run=false to actually create. Accepts draft_id from a previous call (validation failure, dry_run, or a timed-out create) — re-runs from the cached source without re-sending the full JSON. Organization resolution on the real run (dry_run=false): (1) explicit organization_id wins; pass the string 'personal' to save without any org. (2) WEBCAKE_ORG_ID env / x-webcake-org-id header wins. (3) Otherwise list_organizations is called: 0 orgs or lookup fails → personal (no org); exactly 1 org → used automatically (result includes organization_auto_selected:true); 2+ orgs → returns ok:false with the org list and asks the caller to re-call with organization_id. Real writes need WEBCAKE_API_BASE + WEBCAKE_JWT.", {
|
|
35
35
|
source: z
|
|
36
36
|
.any()
|
|
37
37
|
.optional()
|
|
@@ -44,7 +44,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
44
44
|
organization_id: z
|
|
45
45
|
.union([z.string(), z.number()])
|
|
46
46
|
.optional()
|
|
47
|
-
.describe("Organization to create the page in (from list_organizations).
|
|
47
|
+
.describe("Organization to create the page in (id from list_organizations). Pass the string 'personal' to explicitly save without any organization (skips auto-resolution). Omit to fall back to WEBCAKE_ORG_ID env; if that is also unset, the server calls list_organizations: 1 org → auto-selected; 2+ orgs → returns the list and asks you to pick; 0 orgs → personal."),
|
|
48
48
|
dry_run: z
|
|
49
49
|
.boolean()
|
|
50
50
|
.optional()
|
|
@@ -81,7 +81,12 @@ export function registerPersistenceTools(server, domain) {
|
|
|
81
81
|
// Resolve name/org from args, then fall back to what the draft stored.
|
|
82
82
|
const cachedDraft = existingDraftId ? getDraft(existingDraftId) : null;
|
|
83
83
|
const pageName = name ?? cachedDraft?.name ?? "AI Page";
|
|
84
|
-
|
|
84
|
+
// 'personal' is a sentinel meaning "no org, skip auto-resolution".
|
|
85
|
+
const isExplicitPersonal = organization_id != null && `${organization_id}`.toLowerCase() === "personal";
|
|
86
|
+
const explicitOrgId = (organization_id != null && !isExplicitPersonal) ? `${organization_id}` : undefined;
|
|
87
|
+
// On dry_run, use the explicit arg or the draft's stored org (if any).
|
|
88
|
+
const draftOrgId = cachedDraft?.organization_id;
|
|
89
|
+
const orgId = explicitOrgId ?? draftOrgId;
|
|
85
90
|
const result = domain.validate(expanded);
|
|
86
91
|
if (!result.valid) {
|
|
87
92
|
// Cache the failed source so the model can fix ONLY the broken elements via
|
|
@@ -122,6 +127,20 @@ export function registerPersistenceTools(server, domain) {
|
|
|
122
127
|
else {
|
|
123
128
|
existingDraftId = putDraft({ source: expanded, name: pageName, organization_id: orgId });
|
|
124
129
|
}
|
|
130
|
+
// Describe what will happen on the real run given current inputs (cheap, no network call).
|
|
131
|
+
let organizationNote;
|
|
132
|
+
if (isExplicitPersonal) {
|
|
133
|
+
organizationNote = "Will save as a personal page (organization_id:'personal' was passed — auto-resolution skipped).";
|
|
134
|
+
}
|
|
135
|
+
else if (explicitOrgId) {
|
|
136
|
+
organizationNote = `Will use the explicitly supplied organization_id: ${explicitOrgId}.`;
|
|
137
|
+
}
|
|
138
|
+
else if (config?.orgId) {
|
|
139
|
+
organizationNote = `Will use the org from WEBCAKE_ORG_ID env / x-webcake-org-id header: ${config.orgId}.`;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
organizationNote = "No organization_id supplied and no WEBCAKE_ORG_ID env set — on the real run list_organizations will be called: 1 org → auto-selected; 2+ orgs → will ask you to pick; 0 orgs → personal.";
|
|
143
|
+
}
|
|
125
144
|
return text({
|
|
126
145
|
dry_run: true,
|
|
127
146
|
validation: { valid: true, warnings: result.warnings, stats: result.stats },
|
|
@@ -129,6 +148,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
129
148
|
env_ready: missing.length === 0,
|
|
130
149
|
missing_env: missing,
|
|
131
150
|
target_organization_id: orgId ?? config?.orgId ?? null,
|
|
151
|
+
organization_note: organizationNote,
|
|
132
152
|
draft_id: existingDraftId,
|
|
133
153
|
request: config
|
|
134
154
|
? buildRequestRedacted(config, pageName, parsed, orgId)
|
|
@@ -148,18 +168,83 @@ export function registerPersistenceTools(server, domain) {
|
|
|
148
168
|
hint: "Configure WEBCAKE_API_BASE and WEBCAKE_JWT (env), or send the x-webcake-jwt header (remote), then retry.",
|
|
149
169
|
});
|
|
150
170
|
}
|
|
171
|
+
// --- Organization resolution (real run only — dry_run is cheap) ---
|
|
172
|
+
// Resolution order:
|
|
173
|
+
// 1. 'personal' sentinel → no org (skip auto-resolution entirely)
|
|
174
|
+
// 2. Explicit organization_id arg → use as-is
|
|
175
|
+
// 3. WEBCAKE_ORG_ID env / x-webcake-org-id header (config.orgId) → use as-is
|
|
176
|
+
// 4. Call listOrganizations:
|
|
177
|
+
// - 0 orgs or call fails → personal (proceed, add a note in result)
|
|
178
|
+
// - exactly 1 org → auto-select (return organization_auto_selected:true)
|
|
179
|
+
// - 2+ orgs → return ok:false with org list; caller must re-call with organization_id
|
|
180
|
+
let resolvedOrgId = orgId; // may already be set from arg or draft
|
|
181
|
+
let organizationAutoSelected = false;
|
|
182
|
+
let organizationNote;
|
|
183
|
+
if (isExplicitPersonal) {
|
|
184
|
+
// Deliberate personal — skip auto-resolution entirely.
|
|
185
|
+
resolvedOrgId = undefined;
|
|
186
|
+
}
|
|
187
|
+
else if (resolvedOrgId == null && !config.orgId) {
|
|
188
|
+
// No explicit org and no env default → look up orgs.
|
|
189
|
+
const orgResult = await listOrganizations(config);
|
|
190
|
+
if (!orgResult.ok || !orgResult.organizations) {
|
|
191
|
+
// Lookup failed — proceed personal, note the failure.
|
|
192
|
+
console.error(`[create_page] listOrganizations failed: ${orgResult.error}`);
|
|
193
|
+
organizationNote = `org lookup failed (${orgResult.error ?? "unknown error"}) — saving as personal page.`;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const orgs = orgResult.organizations;
|
|
197
|
+
if (orgs.length === 0) {
|
|
198
|
+
// No orgs — personal is the only option.
|
|
199
|
+
}
|
|
200
|
+
else if (orgs.length === 1) {
|
|
201
|
+
// Exactly one org → auto-select.
|
|
202
|
+
resolvedOrgId = `${orgs[0].id}`;
|
|
203
|
+
organizationAutoSelected = true;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Multiple orgs — cannot guess; ask the caller to pick.
|
|
207
|
+
const orgList = orgs.map((o) => ({ id: o.id, name: o.name, is_default: o.is_default }));
|
|
208
|
+
// Cache so the model can retry with organization_id without re-sending source.
|
|
209
|
+
if (existingDraftId) {
|
|
210
|
+
updateDraft(existingDraftId, expanded);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
existingDraftId = putDraft({ source: expanded, name: pageName, organization_id: undefined });
|
|
214
|
+
}
|
|
215
|
+
return text({
|
|
216
|
+
created: false,
|
|
217
|
+
reason: "organization_required",
|
|
218
|
+
organizations: orgList,
|
|
219
|
+
draft_id: existingDraftId,
|
|
220
|
+
error: "This account has multiple organizations. Re-call create_page with organization_id set to one of the listed org ids (or 'personal' to save without an org).",
|
|
221
|
+
hint: `Ask the user which organization to use, then re-call: create_page({ draft_id: "${existingDraftId}", organization_id: "<chosen id>", dry_run: false }).`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (resolvedOrgId == null && config.orgId) {
|
|
227
|
+
// Env default present — use it (already reflected in authHeaders via config.orgId).
|
|
228
|
+
resolvedOrgId = config.orgId;
|
|
229
|
+
}
|
|
151
230
|
// CACHE-FIRST: write to draft cache BEFORE the network call. On timeout or
|
|
152
231
|
// network failure the draft survives and the model can retry without re-sending.
|
|
153
232
|
if (existingDraftId) {
|
|
154
233
|
updateDraft(existingDraftId, expanded);
|
|
155
234
|
}
|
|
156
235
|
else {
|
|
157
|
-
existingDraftId = putDraft({ source: expanded, name: pageName, organization_id:
|
|
236
|
+
existingDraftId = putDraft({ source: expanded, name: pageName, organization_id: resolvedOrgId });
|
|
158
237
|
}
|
|
159
|
-
const outcome = await createPage(config, pageName, parsed,
|
|
238
|
+
const outcome = await createPage(config, pageName, parsed, resolvedOrgId);
|
|
160
239
|
if (outcome.ok) {
|
|
161
240
|
deleteDraft(existingDraftId); // created — drop the draft
|
|
162
|
-
return text({
|
|
241
|
+
return text({
|
|
242
|
+
created: true,
|
|
243
|
+
...outcome,
|
|
244
|
+
warnings: result.warnings,
|
|
245
|
+
...(organizationAutoSelected ? { organization_auto_selected: true } : {}),
|
|
246
|
+
...(organizationNote ? { note: organizationNote } : {}),
|
|
247
|
+
});
|
|
163
248
|
}
|
|
164
249
|
// Failure (including timeout): keep the draft so the model can retry.
|
|
165
250
|
updateDraft(existingDraftId, expanded);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.59",
|
|
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",
|