@charisol/plexo-mcp 1.0.5 → 1.0.7
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/client/plexoClient.js +113 -0
- package/dist/index.js +221 -2
- package/package.json +1 -1
- package/src/client/plexoClient.ts +146 -0
- package/src/index.ts +228 -2
|
@@ -115,6 +115,119 @@ class PlexoClient {
|
|
|
115
115
|
}
|
|
116
116
|
return data;
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Creates a landing page template WITHOUT publishing it — a thin wrapper
|
|
120
|
+
* around the same /api/v1/publish endpoint publishLandingPage() uses,
|
|
121
|
+
* just never forwarding a domain/type even if the caller passed one, so
|
|
122
|
+
* this tool's contract ("never publishes") holds regardless of input.
|
|
123
|
+
*/
|
|
124
|
+
async createLandingPage(params) {
|
|
125
|
+
return this.publishLandingPage({ name: params.name, designJson: params.designJson });
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Links a subdomain or custom domain to an already-existing landing page
|
|
129
|
+
* template (unlike publishLandingPage, which always creates a new one).
|
|
130
|
+
* Used both for a plain create_landing_page template and for the home
|
|
131
|
+
* page of a multi-page site once its sub-pages are ready.
|
|
132
|
+
*/
|
|
133
|
+
async publishExistingLandingPage(params) {
|
|
134
|
+
this.checkAuth();
|
|
135
|
+
const type = params.type || (params.domain.includes(".") ? "CUSTOM" : "SUBDOMAIN");
|
|
136
|
+
const res = await fetch(`${this.baseUrl}/api/v1/domains`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: this.getHeaders(),
|
|
139
|
+
body: JSON.stringify({ templateId: params.templateId, domain: params.domain, type }),
|
|
140
|
+
});
|
|
141
|
+
const data = (await res.json());
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
throw new Error(data?.error || `Publishing failed with status ${res.status}`);
|
|
144
|
+
}
|
|
145
|
+
return data;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Creates a new page nested under an existing landing page (its home page
|
|
149
|
+
* for a top-level page, or another sub-page's id to nest deeper) — how a
|
|
150
|
+
* single landing page becomes a multi-page site. Starts blank; use
|
|
151
|
+
* updateTemplate() with the returned templateId to add content.
|
|
152
|
+
*/
|
|
153
|
+
async createLandingPageSubpage(params) {
|
|
154
|
+
this.checkAuth();
|
|
155
|
+
const res = await fetch(`${this.baseUrl}/api/templates`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: this.getHeaders(),
|
|
158
|
+
body: JSON.stringify({ name: params.name, parentId: params.parentTemplateId, slug: params.slug }),
|
|
159
|
+
});
|
|
160
|
+
const data = (await res.json());
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
throw new Error(data?.error || `Sub-page creation failed with status ${res.status}`);
|
|
163
|
+
}
|
|
164
|
+
return data;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Returns every page belonging to the same multi-page site as templateId
|
|
168
|
+
* — its home page plus all nested sub-pages. templateId can be the home
|
|
169
|
+
* page or any sub-page; either way the whole tree comes back.
|
|
170
|
+
*/
|
|
171
|
+
async getLandingPagePages(templateId) {
|
|
172
|
+
this.checkAuth();
|
|
173
|
+
const res = await fetch(`${this.baseUrl}/api/templates/${encodeURIComponent(templateId)}/pages`, {
|
|
174
|
+
method: "GET",
|
|
175
|
+
headers: this.getHeaders(),
|
|
176
|
+
});
|
|
177
|
+
const data = (await res.json());
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
throw new Error(data?.error || `Failed to fetch page tree (Status ${res.status})`);
|
|
180
|
+
}
|
|
181
|
+
return data;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Renames a page, changes its URL segment, moves it under a different
|
|
185
|
+
* parent, or reorders it among siblings. Does not touch page content —
|
|
186
|
+
* use updateTemplate() for that.
|
|
187
|
+
*/
|
|
188
|
+
async updateLandingPagePage(params) {
|
|
189
|
+
this.checkAuth();
|
|
190
|
+
const { templateId, parentTemplateId, ...rest } = params;
|
|
191
|
+
const body = { ...rest };
|
|
192
|
+
if (parentTemplateId !== undefined)
|
|
193
|
+
body.parentId = parentTemplateId;
|
|
194
|
+
const res = await fetch(`${this.baseUrl}/api/templates/${encodeURIComponent(templateId)}`, {
|
|
195
|
+
method: "PATCH",
|
|
196
|
+
headers: this.getHeaders(),
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
});
|
|
199
|
+
const data = (await res.json());
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
throw new Error(data?.error || `Page update failed with status ${res.status}`);
|
|
202
|
+
}
|
|
203
|
+
return data;
|
|
204
|
+
}
|
|
205
|
+
/** Deletes a page. Sub-pages nested under it are cascade-deleted too. */
|
|
206
|
+
async deleteLandingPagePage(templateId) {
|
|
207
|
+
this.checkAuth();
|
|
208
|
+
const res = await fetch(`${this.baseUrl}/api/templates/${encodeURIComponent(templateId)}`, {
|
|
209
|
+
method: "DELETE",
|
|
210
|
+
headers: this.getHeaders(),
|
|
211
|
+
});
|
|
212
|
+
const data = (await res.json());
|
|
213
|
+
if (!res.ok) {
|
|
214
|
+
throw new Error(data?.error || `Page deletion failed with status ${res.status}`);
|
|
215
|
+
}
|
|
216
|
+
return data;
|
|
217
|
+
}
|
|
218
|
+
/** Clones a sub-page's content into a new sibling page under the same parent. */
|
|
219
|
+
async duplicateLandingPagePage(templateId) {
|
|
220
|
+
this.checkAuth();
|
|
221
|
+
const res = await fetch(`${this.baseUrl}/api/templates/${encodeURIComponent(templateId)}/duplicate`, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: this.getHeaders(),
|
|
224
|
+
});
|
|
225
|
+
const data = (await res.json());
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
throw new Error(data?.error || `Page duplication failed with status ${res.status}`);
|
|
228
|
+
}
|
|
229
|
+
return data;
|
|
230
|
+
}
|
|
118
231
|
async listTemplates() {
|
|
119
232
|
this.checkAuth();
|
|
120
233
|
const res = await fetch(`${this.baseUrl}/api/templates`, {
|
package/dist/index.js
CHANGED
|
@@ -132,7 +132,7 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
|
|
|
132
132
|
enum: ["heading", "paragraph", "text", "button", "card", "image", "menu", "social", "divider", "spacer", "form_container", "input", "textarea", "select", "carousel", "html", "icon", "table", "timer", "video"],
|
|
133
133
|
},
|
|
134
134
|
style: { type: "object" },
|
|
135
|
-
attributes: { type: "object", description: "Component attributes (text, href, src, title, description, links, fields)." },
|
|
135
|
+
attributes: { type: "object", description: "Component attributes (text, href, src, title, description, links, fields). For type 'html', raw markup goes under 'htmlContent' — NOT 'text'." },
|
|
136
136
|
},
|
|
137
137
|
},
|
|
138
138
|
},
|
|
@@ -226,12 +226,13 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page — designJso
|
|
|
226
226
|
style: { type: "object" },
|
|
227
227
|
attributes: {
|
|
228
228
|
type: "object",
|
|
229
|
-
description: "Component attributes. IMPORTANT: text content for 'heading'/'paragraph'/'text'/'button' elements goes under the key 'text' (NOT 'content'). Images use 'src'/'alt'. Buttons/links use 'href'.",
|
|
229
|
+
description: "Component attributes. IMPORTANT: text content for 'heading'/'paragraph'/'text'/'button' elements goes under the key 'text' (NOT 'content'). Images use 'src'/'alt'. Buttons/links use 'href'. Raw markup for type 'html' goes under 'htmlContent' (NOT 'text').",
|
|
230
230
|
properties: {
|
|
231
231
|
text: { type: "string", description: "Visible text for heading/paragraph/text/button elements." },
|
|
232
232
|
href: { type: "string", description: "Link URL for button/card elements." },
|
|
233
233
|
src: { type: "string", description: "Image URL for image elements." },
|
|
234
234
|
alt: { type: "string", description: "Alt text for image elements." },
|
|
235
|
+
htmlContent: { type: "string", description: "Raw HTML markup for type 'html' elements only." },
|
|
235
236
|
},
|
|
236
237
|
},
|
|
237
238
|
},
|
|
@@ -300,6 +301,147 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page/create_email_
|
|
|
300
301
|
required: ["templateId", "designJson"],
|
|
301
302
|
},
|
|
302
303
|
},
|
|
304
|
+
{
|
|
305
|
+
name: "create_landing_page",
|
|
306
|
+
description: `Creates and saves a landing page template WITHOUT publishing it to a domain — use this when the user wants to build a page (or start a multi-page site) first and go live later. Call publish_existing_landing_page afterward with the returned templateId to link a subdomain or custom domain whenever they're ready. To create a page AND publish it in one step, use publish_landing_page instead.
|
|
307
|
+
|
|
308
|
+
Uses the SAME fully-hydrated layout schema as publish_landing_page — see its description for a full worked example.`,
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
name: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "Title of the landing page (e.g. 'Kicks Store').",
|
|
315
|
+
},
|
|
316
|
+
designJson: {
|
|
317
|
+
type: "object",
|
|
318
|
+
description: "Plexo layout schema object — same shape as publish_landing_page's designJson.",
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
required: ["designJson"],
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "publish_existing_landing_page",
|
|
326
|
+
description: `Links a subdomain or custom domain to an already-existing landing page template — for a template created via create_landing_page, or the home page of a multi-page site (see create_landing_page_subpage) once its sub-pages are ready. Unlike publish_landing_page, this does NOT create a new template — it publishes the one you already have. Every sub-page nested under templateId automatically becomes reachable at this domain too (e.g. domain.com/about) — no separate publish call is needed per sub-page.`,
|
|
327
|
+
inputSchema: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
templateId: {
|
|
331
|
+
type: "string",
|
|
332
|
+
description: "ID of an existing landing page template to publish — must be a home page (no parent), not a sub-page.",
|
|
333
|
+
},
|
|
334
|
+
domain: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: "Subdomain slug (e.g. 'kicks') or custom domain ('kicks.com').",
|
|
337
|
+
},
|
|
338
|
+
type: {
|
|
339
|
+
type: "string",
|
|
340
|
+
enum: ["SUBDOMAIN", "CUSTOM"],
|
|
341
|
+
description: "Domain routing type. Inferred from the domain string if omitted.",
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
required: ["templateId", "domain"],
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "create_landing_page_subpage",
|
|
349
|
+
description: `Adds a new page nested under an existing landing page, reachable at <site domain>/<slug> — or nested deeper (e.g. /blog/post-1) if parentTemplateId is itself a sub-page rather than the home page. This is how a single landing page becomes a multi-page site. No separate publish step is needed for sub-pages: they automatically become reachable once the site's home page has a domain linked (see publish_existing_landing_page). Requires an Ultra subscription plan — sub-pages are otherwise unlimited per site once enabled.
|
|
350
|
+
|
|
351
|
+
The new page starts blank — use update_template with the returned templateId to add content.`,
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
parentTemplateId: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: "ID of the page this new page should be nested under — the site's home page for a top-level page (e.g. /about), or another sub-page's ID to nest deeper (e.g. a 'Blog' page's ID to create /blog/post-1).",
|
|
358
|
+
},
|
|
359
|
+
name: {
|
|
360
|
+
type: "string",
|
|
361
|
+
description: "Friendly name of the page (e.g. 'About Us').",
|
|
362
|
+
},
|
|
363
|
+
slug: {
|
|
364
|
+
type: "string",
|
|
365
|
+
description: "Optional URL segment (e.g. 'about-us'). Auto-generated from name if omitted; auto-suffixed (about-us-2, ...) if it collides with a sibling page under the same parent.",
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
required: ["parentTemplateId", "name"],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "get_landing_page_pages",
|
|
373
|
+
description: "Returns every page belonging to the same multi-page site as the given template — its home page plus all nested sub-pages, each with its name, URL segment (slug), parent, and full resolved path (e.g. '/blog/post-1'). Pass the home page's ID or any sub-page's ID — either way you get the whole tree back. Use this before create_landing_page_subpage/update_landing_page_page when you need to know what pages already exist or what a page's current slug/parent is.",
|
|
374
|
+
inputSchema: {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: {
|
|
377
|
+
templateId: {
|
|
378
|
+
type: "string",
|
|
379
|
+
description: "ID of the site's home page, or any sub-page within it.",
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
required: ["templateId"],
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "update_landing_page_page",
|
|
387
|
+
description: `Renames a page, changes its URL segment (slug), moves it under a different parent page, or reorders it among its siblings. The home page of a site (no parent) can be renamed but has no slug of its own and can't be re-parented — use publish_existing_landing_page to manage its domain instead.
|
|
388
|
+
|
|
389
|
+
To edit a page's CONTENT (designJson), use update_template with the same templateId instead — this tool only manages the page's place in the site's structure, not what's on it.`,
|
|
390
|
+
inputSchema: {
|
|
391
|
+
type: "object",
|
|
392
|
+
properties: {
|
|
393
|
+
templateId: {
|
|
394
|
+
type: "string",
|
|
395
|
+
description: "ID of the page to update.",
|
|
396
|
+
},
|
|
397
|
+
name: {
|
|
398
|
+
type: "string",
|
|
399
|
+
description: "Optional new name.",
|
|
400
|
+
},
|
|
401
|
+
slug: {
|
|
402
|
+
type: "string",
|
|
403
|
+
description: "Optional new URL segment. Not allowed for the home page.",
|
|
404
|
+
},
|
|
405
|
+
parentTemplateId: {
|
|
406
|
+
type: "string",
|
|
407
|
+
description: "Optional new parent page ID, to move this page elsewhere in the site's tree. Rejected if it would create a cycle (e.g. moving a page underneath its own sub-page).",
|
|
408
|
+
},
|
|
409
|
+
order: {
|
|
410
|
+
type: "number",
|
|
411
|
+
description: "Optional sort position among sibling pages under the same parent (lower = earlier).",
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
required: ["templateId"],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "delete_landing_page_page",
|
|
419
|
+
description: "Deletes a page. If it has sub-pages nested under it, they're deleted too (cascade) — the response reports how many. Deleting a site's home page deletes the whole site, including every page nested under it and its published domain link — this can't be undone, confirm with the user first.",
|
|
420
|
+
inputSchema: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
templateId: {
|
|
424
|
+
type: "string",
|
|
425
|
+
description: "ID of the page to delete.",
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
required: ["templateId"],
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "duplicate_landing_page_page",
|
|
433
|
+
description: "Clones a sub-page's content into a new sibling page under the same parent — handy for building several similar pages (e.g. multiple product pages) without starting blank. Not available for a site's home page. Requires an Ultra subscription plan.",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {
|
|
437
|
+
templateId: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "ID of the sub-page to duplicate — must not be a home page.",
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
required: ["templateId"],
|
|
443
|
+
},
|
|
444
|
+
},
|
|
303
445
|
{
|
|
304
446
|
name: "list_landing_pages",
|
|
305
447
|
description: "Lists all saved landing page templates and published domain URLs in the user's Plexo account.",
|
|
@@ -407,6 +549,83 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
407
549
|
],
|
|
408
550
|
};
|
|
409
551
|
}
|
|
552
|
+
case "create_landing_page": {
|
|
553
|
+
const result = await client.createLandingPage(args);
|
|
554
|
+
return {
|
|
555
|
+
content: [
|
|
556
|
+
{
|
|
557
|
+
type: "text",
|
|
558
|
+
text: JSON.stringify(result, null, 2),
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
case "publish_existing_landing_page": {
|
|
564
|
+
const result = await client.publishExistingLandingPage(args);
|
|
565
|
+
return {
|
|
566
|
+
content: [
|
|
567
|
+
{
|
|
568
|
+
type: "text",
|
|
569
|
+
text: JSON.stringify(result, null, 2),
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
case "create_landing_page_subpage": {
|
|
575
|
+
const result = await client.createLandingPageSubpage(args);
|
|
576
|
+
return {
|
|
577
|
+
content: [
|
|
578
|
+
{
|
|
579
|
+
type: "text",
|
|
580
|
+
text: JSON.stringify(result, null, 2),
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
case "get_landing_page_pages": {
|
|
586
|
+
const result = await client.getLandingPagePages(args?.templateId);
|
|
587
|
+
return {
|
|
588
|
+
content: [
|
|
589
|
+
{
|
|
590
|
+
type: "text",
|
|
591
|
+
text: JSON.stringify(result, null, 2),
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
case "update_landing_page_page": {
|
|
597
|
+
const result = await client.updateLandingPagePage(args);
|
|
598
|
+
return {
|
|
599
|
+
content: [
|
|
600
|
+
{
|
|
601
|
+
type: "text",
|
|
602
|
+
text: JSON.stringify(result, null, 2),
|
|
603
|
+
},
|
|
604
|
+
],
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
case "delete_landing_page_page": {
|
|
608
|
+
const result = await client.deleteLandingPagePage(args?.templateId);
|
|
609
|
+
return {
|
|
610
|
+
content: [
|
|
611
|
+
{
|
|
612
|
+
type: "text",
|
|
613
|
+
text: JSON.stringify(result, null, 2),
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
case "duplicate_landing_page_page": {
|
|
619
|
+
const result = await client.duplicateLandingPagePage(args?.templateId);
|
|
620
|
+
return {
|
|
621
|
+
content: [
|
|
622
|
+
{
|
|
623
|
+
type: "text",
|
|
624
|
+
text: JSON.stringify(result, null, 2),
|
|
625
|
+
},
|
|
626
|
+
],
|
|
627
|
+
};
|
|
628
|
+
}
|
|
410
629
|
case "list_landing_pages": {
|
|
411
630
|
const data = await client.listTemplates();
|
|
412
631
|
const landingPages = (data.templates || []).filter((t) => t.kind === "LANDING_PAGE");
|
package/package.json
CHANGED
|
@@ -156,6 +156,152 @@ export class PlexoClient {
|
|
|
156
156
|
return data;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Creates a landing page template WITHOUT publishing it — a thin wrapper
|
|
161
|
+
* around the same /api/v1/publish endpoint publishLandingPage() uses,
|
|
162
|
+
* just never forwarding a domain/type even if the caller passed one, so
|
|
163
|
+
* this tool's contract ("never publishes") holds regardless of input.
|
|
164
|
+
*/
|
|
165
|
+
async createLandingPage(params: { name?: string; designJson: Record<string, any> }): Promise<any> {
|
|
166
|
+
return this.publishLandingPage({ name: params.name, designJson: params.designJson });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Links a subdomain or custom domain to an already-existing landing page
|
|
171
|
+
* template (unlike publishLandingPage, which always creates a new one).
|
|
172
|
+
* Used both for a plain create_landing_page template and for the home
|
|
173
|
+
* page of a multi-page site once its sub-pages are ready.
|
|
174
|
+
*/
|
|
175
|
+
async publishExistingLandingPage(params: {
|
|
176
|
+
templateId: string;
|
|
177
|
+
domain: string;
|
|
178
|
+
type?: "SUBDOMAIN" | "CUSTOM";
|
|
179
|
+
}): Promise<any> {
|
|
180
|
+
this.checkAuth();
|
|
181
|
+
const type = params.type || (params.domain.includes(".") ? "CUSTOM" : "SUBDOMAIN");
|
|
182
|
+
const res = await fetch(`${this.baseUrl}/api/v1/domains`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: this.getHeaders(),
|
|
185
|
+
body: JSON.stringify({ templateId: params.templateId, domain: params.domain, type }),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const data = (await res.json()) as any;
|
|
189
|
+
if (!res.ok) {
|
|
190
|
+
throw new Error(data?.error || `Publishing failed with status ${res.status}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return data;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Creates a new page nested under an existing landing page (its home page
|
|
198
|
+
* for a top-level page, or another sub-page's id to nest deeper) — how a
|
|
199
|
+
* single landing page becomes a multi-page site. Starts blank; use
|
|
200
|
+
* updateTemplate() with the returned templateId to add content.
|
|
201
|
+
*/
|
|
202
|
+
async createLandingPageSubpage(params: {
|
|
203
|
+
parentTemplateId: string;
|
|
204
|
+
name: string;
|
|
205
|
+
slug?: string;
|
|
206
|
+
}): Promise<any> {
|
|
207
|
+
this.checkAuth();
|
|
208
|
+
const res = await fetch(`${this.baseUrl}/api/templates`, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: this.getHeaders(),
|
|
211
|
+
body: JSON.stringify({ name: params.name, parentId: params.parentTemplateId, slug: params.slug }),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const data = (await res.json()) as any;
|
|
215
|
+
if (!res.ok) {
|
|
216
|
+
throw new Error(data?.error || `Sub-page creation failed with status ${res.status}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return data;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Returns every page belonging to the same multi-page site as templateId
|
|
224
|
+
* — its home page plus all nested sub-pages. templateId can be the home
|
|
225
|
+
* page or any sub-page; either way the whole tree comes back.
|
|
226
|
+
*/
|
|
227
|
+
async getLandingPagePages(templateId: string): Promise<any> {
|
|
228
|
+
this.checkAuth();
|
|
229
|
+
const res = await fetch(`${this.baseUrl}/api/templates/${encodeURIComponent(templateId)}/pages`, {
|
|
230
|
+
method: "GET",
|
|
231
|
+
headers: this.getHeaders(),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const data = (await res.json()) as any;
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
throw new Error(data?.error || `Failed to fetch page tree (Status ${res.status})`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return data;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Renames a page, changes its URL segment, moves it under a different
|
|
244
|
+
* parent, or reorders it among siblings. Does not touch page content —
|
|
245
|
+
* use updateTemplate() for that.
|
|
246
|
+
*/
|
|
247
|
+
async updateLandingPagePage(params: {
|
|
248
|
+
templateId: string;
|
|
249
|
+
name?: string;
|
|
250
|
+
slug?: string;
|
|
251
|
+
parentTemplateId?: string;
|
|
252
|
+
order?: number;
|
|
253
|
+
}): Promise<any> {
|
|
254
|
+
this.checkAuth();
|
|
255
|
+
const { templateId, parentTemplateId, ...rest } = params;
|
|
256
|
+
const body: Record<string, any> = { ...rest };
|
|
257
|
+
if (parentTemplateId !== undefined) body.parentId = parentTemplateId;
|
|
258
|
+
|
|
259
|
+
const res = await fetch(`${this.baseUrl}/api/templates/${encodeURIComponent(templateId)}`, {
|
|
260
|
+
method: "PATCH",
|
|
261
|
+
headers: this.getHeaders(),
|
|
262
|
+
body: JSON.stringify(body),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const data = (await res.json()) as any;
|
|
266
|
+
if (!res.ok) {
|
|
267
|
+
throw new Error(data?.error || `Page update failed with status ${res.status}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return data;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Deletes a page. Sub-pages nested under it are cascade-deleted too. */
|
|
274
|
+
async deleteLandingPagePage(templateId: string): Promise<any> {
|
|
275
|
+
this.checkAuth();
|
|
276
|
+
const res = await fetch(`${this.baseUrl}/api/templates/${encodeURIComponent(templateId)}`, {
|
|
277
|
+
method: "DELETE",
|
|
278
|
+
headers: this.getHeaders(),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const data = (await res.json()) as any;
|
|
282
|
+
if (!res.ok) {
|
|
283
|
+
throw new Error(data?.error || `Page deletion failed with status ${res.status}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return data;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Clones a sub-page's content into a new sibling page under the same parent. */
|
|
290
|
+
async duplicateLandingPagePage(templateId: string): Promise<any> {
|
|
291
|
+
this.checkAuth();
|
|
292
|
+
const res = await fetch(`${this.baseUrl}/api/templates/${encodeURIComponent(templateId)}/duplicate`, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: this.getHeaders(),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const data = (await res.json()) as any;
|
|
298
|
+
if (!res.ok) {
|
|
299
|
+
throw new Error(data?.error || `Page duplication failed with status ${res.status}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return data;
|
|
303
|
+
}
|
|
304
|
+
|
|
159
305
|
async listTemplates(): Promise<any> {
|
|
160
306
|
this.checkAuth();
|
|
161
307
|
const res = await fetch(`${this.baseUrl}/api/templates`, {
|
package/src/index.ts
CHANGED
|
@@ -139,7 +139,7 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
|
|
|
139
139
|
enum: ["heading", "paragraph", "text", "button", "card", "image", "menu", "social", "divider", "spacer", "form_container", "input", "textarea", "select", "carousel", "html", "icon", "table", "timer", "video"],
|
|
140
140
|
},
|
|
141
141
|
style: { type: "object" },
|
|
142
|
-
attributes: { type: "object", description: "Component attributes (text, href, src, title, description, links, fields)." },
|
|
142
|
+
attributes: { type: "object", description: "Component attributes (text, href, src, title, description, links, fields). For type 'html', raw markup goes under 'htmlContent' — NOT 'text'." },
|
|
143
143
|
},
|
|
144
144
|
},
|
|
145
145
|
},
|
|
@@ -233,12 +233,13 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page — designJso
|
|
|
233
233
|
style: { type: "object" },
|
|
234
234
|
attributes: {
|
|
235
235
|
type: "object",
|
|
236
|
-
description: "Component attributes. IMPORTANT: text content for 'heading'/'paragraph'/'text'/'button' elements goes under the key 'text' (NOT 'content'). Images use 'src'/'alt'. Buttons/links use 'href'.",
|
|
236
|
+
description: "Component attributes. IMPORTANT: text content for 'heading'/'paragraph'/'text'/'button' elements goes under the key 'text' (NOT 'content'). Images use 'src'/'alt'. Buttons/links use 'href'. Raw markup for type 'html' goes under 'htmlContent' (NOT 'text').",
|
|
237
237
|
properties: {
|
|
238
238
|
text: { type: "string", description: "Visible text for heading/paragraph/text/button elements." },
|
|
239
239
|
href: { type: "string", description: "Link URL for button/card elements." },
|
|
240
240
|
src: { type: "string", description: "Image URL for image elements." },
|
|
241
241
|
alt: { type: "string", description: "Alt text for image elements." },
|
|
242
|
+
htmlContent: { type: "string", description: "Raw HTML markup for type 'html' elements only." },
|
|
242
243
|
},
|
|
243
244
|
},
|
|
244
245
|
},
|
|
@@ -307,6 +308,147 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page/create_email_
|
|
|
307
308
|
required: ["templateId", "designJson"],
|
|
308
309
|
},
|
|
309
310
|
},
|
|
311
|
+
{
|
|
312
|
+
name: "create_landing_page",
|
|
313
|
+
description: `Creates and saves a landing page template WITHOUT publishing it to a domain — use this when the user wants to build a page (or start a multi-page site) first and go live later. Call publish_existing_landing_page afterward with the returned templateId to link a subdomain or custom domain whenever they're ready. To create a page AND publish it in one step, use publish_landing_page instead.
|
|
314
|
+
|
|
315
|
+
Uses the SAME fully-hydrated layout schema as publish_landing_page — see its description for a full worked example.`,
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: "object",
|
|
318
|
+
properties: {
|
|
319
|
+
name: {
|
|
320
|
+
type: "string",
|
|
321
|
+
description: "Title of the landing page (e.g. 'Kicks Store').",
|
|
322
|
+
},
|
|
323
|
+
designJson: {
|
|
324
|
+
type: "object",
|
|
325
|
+
description: "Plexo layout schema object — same shape as publish_landing_page's designJson.",
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
required: ["designJson"],
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "publish_existing_landing_page",
|
|
333
|
+
description: `Links a subdomain or custom domain to an already-existing landing page template — for a template created via create_landing_page, or the home page of a multi-page site (see create_landing_page_subpage) once its sub-pages are ready. Unlike publish_landing_page, this does NOT create a new template — it publishes the one you already have. Every sub-page nested under templateId automatically becomes reachable at this domain too (e.g. domain.com/about) — no separate publish call is needed per sub-page.`,
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: "object",
|
|
336
|
+
properties: {
|
|
337
|
+
templateId: {
|
|
338
|
+
type: "string",
|
|
339
|
+
description: "ID of an existing landing page template to publish — must be a home page (no parent), not a sub-page.",
|
|
340
|
+
},
|
|
341
|
+
domain: {
|
|
342
|
+
type: "string",
|
|
343
|
+
description: "Subdomain slug (e.g. 'kicks') or custom domain ('kicks.com').",
|
|
344
|
+
},
|
|
345
|
+
type: {
|
|
346
|
+
type: "string",
|
|
347
|
+
enum: ["SUBDOMAIN", "CUSTOM"],
|
|
348
|
+
description: "Domain routing type. Inferred from the domain string if omitted.",
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
required: ["templateId", "domain"],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "create_landing_page_subpage",
|
|
356
|
+
description: `Adds a new page nested under an existing landing page, reachable at <site domain>/<slug> — or nested deeper (e.g. /blog/post-1) if parentTemplateId is itself a sub-page rather than the home page. This is how a single landing page becomes a multi-page site. No separate publish step is needed for sub-pages: they automatically become reachable once the site's home page has a domain linked (see publish_existing_landing_page). Requires an Ultra subscription plan — sub-pages are otherwise unlimited per site once enabled.
|
|
357
|
+
|
|
358
|
+
The new page starts blank — use update_template with the returned templateId to add content.`,
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
parentTemplateId: {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "ID of the page this new page should be nested under — the site's home page for a top-level page (e.g. /about), or another sub-page's ID to nest deeper (e.g. a 'Blog' page's ID to create /blog/post-1).",
|
|
365
|
+
},
|
|
366
|
+
name: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "Friendly name of the page (e.g. 'About Us').",
|
|
369
|
+
},
|
|
370
|
+
slug: {
|
|
371
|
+
type: "string",
|
|
372
|
+
description: "Optional URL segment (e.g. 'about-us'). Auto-generated from name if omitted; auto-suffixed (about-us-2, ...) if it collides with a sibling page under the same parent.",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
required: ["parentTemplateId", "name"],
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "get_landing_page_pages",
|
|
380
|
+
description: "Returns every page belonging to the same multi-page site as the given template — its home page plus all nested sub-pages, each with its name, URL segment (slug), parent, and full resolved path (e.g. '/blog/post-1'). Pass the home page's ID or any sub-page's ID — either way you get the whole tree back. Use this before create_landing_page_subpage/update_landing_page_page when you need to know what pages already exist or what a page's current slug/parent is.",
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: "object",
|
|
383
|
+
properties: {
|
|
384
|
+
templateId: {
|
|
385
|
+
type: "string",
|
|
386
|
+
description: "ID of the site's home page, or any sub-page within it.",
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
required: ["templateId"],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "update_landing_page_page",
|
|
394
|
+
description: `Renames a page, changes its URL segment (slug), moves it under a different parent page, or reorders it among its siblings. The home page of a site (no parent) can be renamed but has no slug of its own and can't be re-parented — use publish_existing_landing_page to manage its domain instead.
|
|
395
|
+
|
|
396
|
+
To edit a page's CONTENT (designJson), use update_template with the same templateId instead — this tool only manages the page's place in the site's structure, not what's on it.`,
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
templateId: {
|
|
401
|
+
type: "string",
|
|
402
|
+
description: "ID of the page to update.",
|
|
403
|
+
},
|
|
404
|
+
name: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "Optional new name.",
|
|
407
|
+
},
|
|
408
|
+
slug: {
|
|
409
|
+
type: "string",
|
|
410
|
+
description: "Optional new URL segment. Not allowed for the home page.",
|
|
411
|
+
},
|
|
412
|
+
parentTemplateId: {
|
|
413
|
+
type: "string",
|
|
414
|
+
description: "Optional new parent page ID, to move this page elsewhere in the site's tree. Rejected if it would create a cycle (e.g. moving a page underneath its own sub-page).",
|
|
415
|
+
},
|
|
416
|
+
order: {
|
|
417
|
+
type: "number",
|
|
418
|
+
description: "Optional sort position among sibling pages under the same parent (lower = earlier).",
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
required: ["templateId"],
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "delete_landing_page_page",
|
|
426
|
+
description: "Deletes a page. If it has sub-pages nested under it, they're deleted too (cascade) — the response reports how many. Deleting a site's home page deletes the whole site, including every page nested under it and its published domain link — this can't be undone, confirm with the user first.",
|
|
427
|
+
inputSchema: {
|
|
428
|
+
type: "object",
|
|
429
|
+
properties: {
|
|
430
|
+
templateId: {
|
|
431
|
+
type: "string",
|
|
432
|
+
description: "ID of the page to delete.",
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
required: ["templateId"],
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "duplicate_landing_page_page",
|
|
440
|
+
description: "Clones a sub-page's content into a new sibling page under the same parent — handy for building several similar pages (e.g. multiple product pages) without starting blank. Not available for a site's home page. Requires an Ultra subscription plan.",
|
|
441
|
+
inputSchema: {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: {
|
|
444
|
+
templateId: {
|
|
445
|
+
type: "string",
|
|
446
|
+
description: "ID of the sub-page to duplicate — must not be a home page.",
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
required: ["templateId"],
|
|
450
|
+
},
|
|
451
|
+
},
|
|
310
452
|
{
|
|
311
453
|
name: "list_landing_pages",
|
|
312
454
|
description: "Lists all saved landing page templates and published domain URLs in the user's Plexo account.",
|
|
@@ -420,6 +562,90 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
420
562
|
};
|
|
421
563
|
}
|
|
422
564
|
|
|
565
|
+
case "create_landing_page": {
|
|
566
|
+
const result = await client.createLandingPage(args as any);
|
|
567
|
+
return {
|
|
568
|
+
content: [
|
|
569
|
+
{
|
|
570
|
+
type: "text",
|
|
571
|
+
text: JSON.stringify(result, null, 2),
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
case "publish_existing_landing_page": {
|
|
578
|
+
const result = await client.publishExistingLandingPage(args as any);
|
|
579
|
+
return {
|
|
580
|
+
content: [
|
|
581
|
+
{
|
|
582
|
+
type: "text",
|
|
583
|
+
text: JSON.stringify(result, null, 2),
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
case "create_landing_page_subpage": {
|
|
590
|
+
const result = await client.createLandingPageSubpage(args as any);
|
|
591
|
+
return {
|
|
592
|
+
content: [
|
|
593
|
+
{
|
|
594
|
+
type: "text",
|
|
595
|
+
text: JSON.stringify(result, null, 2),
|
|
596
|
+
},
|
|
597
|
+
],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
case "get_landing_page_pages": {
|
|
602
|
+
const result = await client.getLandingPagePages((args as any)?.templateId);
|
|
603
|
+
return {
|
|
604
|
+
content: [
|
|
605
|
+
{
|
|
606
|
+
type: "text",
|
|
607
|
+
text: JSON.stringify(result, null, 2),
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
case "update_landing_page_page": {
|
|
614
|
+
const result = await client.updateLandingPagePage(args as any);
|
|
615
|
+
return {
|
|
616
|
+
content: [
|
|
617
|
+
{
|
|
618
|
+
type: "text",
|
|
619
|
+
text: JSON.stringify(result, null, 2),
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
case "delete_landing_page_page": {
|
|
626
|
+
const result = await client.deleteLandingPagePage((args as any)?.templateId);
|
|
627
|
+
return {
|
|
628
|
+
content: [
|
|
629
|
+
{
|
|
630
|
+
type: "text",
|
|
631
|
+
text: JSON.stringify(result, null, 2),
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
case "duplicate_landing_page_page": {
|
|
638
|
+
const result = await client.duplicateLandingPagePage((args as any)?.templateId);
|
|
639
|
+
return {
|
|
640
|
+
content: [
|
|
641
|
+
{
|
|
642
|
+
type: "text",
|
|
643
|
+
text: JSON.stringify(result, null, 2),
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
423
649
|
case "list_landing_pages": {
|
|
424
650
|
const data = await client.listTemplates();
|
|
425
651
|
const landingPages = (data.templates || []).filter(
|