@charisol/plexo-mcp 1.0.7 → 1.0.8

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.
@@ -355,4 +355,134 @@ export class PlexoClient {
355
355
 
356
356
  return data;
357
357
  }
358
+
359
+ /** Fetches a single template's full details, including designJson — used by the blog
360
+ * layout tools to read back a layout template's current content (there's no other
361
+ * templateId-scoped GET in the public API; list endpoints only return metadata). */
362
+ async getTemplate(templateId: string): Promise<any> {
363
+ this.checkAuth();
364
+ const res = await fetch(`${this.baseUrl}/api/v1/templates/${encodeURIComponent(templateId)}`, {
365
+ method: "GET",
366
+ headers: this.getHeaders(),
367
+ });
368
+
369
+ const data = (await res.json()) as any;
370
+ if (!res.ok) {
371
+ throw new Error(data?.error || `Failed to fetch template (Status ${res.status})`);
372
+ }
373
+
374
+ return data;
375
+ }
376
+
377
+ /** Shared request helper for every /api/blog/{templateId}/... route below — these are
378
+ * the same admin endpoints the Plexo dashboard's own blog UI calls, and (like every
379
+ * other route in this client) accept the same Bearer/x-api-key auth as the rest of the
380
+ * public API (see resolveUser/resolveBlogAdminSite server-side). */
381
+ private async blogFetch(method: string, templateId: string, path: string, body?: unknown): Promise<any> {
382
+ this.checkAuth();
383
+ const res = await fetch(`${this.baseUrl}/api/blog/${encodeURIComponent(templateId)}${path}`, {
384
+ method,
385
+ headers: this.getHeaders(),
386
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
387
+ });
388
+
389
+ const data = (await res.json().catch(() => ({}))) as any;
390
+ if (!res.ok) {
391
+ throw new Error(data?.error || `Blog request failed (Status ${res.status})`);
392
+ }
393
+ return data;
394
+ }
395
+
396
+ /** Idempotent — returns the existing layout template id if one's already attached to
397
+ * this kind rather than creating a duplicate. Pair with updateTemplate() to set its
398
+ * designJson (see blogTools.ts's designBlogLayout). */
399
+ async ensureBlogLayout(templateId: string, kind: "post" | "listing"): Promise<any> {
400
+ return this.blogFetch("POST", templateId, `/layout/${kind}`);
401
+ }
402
+
403
+ /** Detaches the layout (reverts to the default theme) without deleting the underlying Template row. */
404
+ async resetBlogLayout(templateId: string, kind: "post" | "listing"): Promise<any> {
405
+ return this.blogFetch("DELETE", templateId, `/layout/${kind}`);
406
+ }
407
+
408
+ async getBlogSite(templateId: string): Promise<any> {
409
+ return this.blogFetch("GET", templateId, "/site");
410
+ }
411
+
412
+ async updateBlogSite(templateId: string, patch: Record<string, any>): Promise<any> {
413
+ return this.blogFetch("PATCH", templateId, "/site", patch);
414
+ }
415
+
416
+ async listBlogPosts(templateId: string, params: { status?: string; search?: string; categoryId?: string } = {}): Promise<any> {
417
+ const query = new URLSearchParams();
418
+ if (params.status) query.set("status", params.status);
419
+ if (params.search) query.set("search", params.search);
420
+ if (params.categoryId) query.set("categoryId", params.categoryId);
421
+ const qs = query.toString();
422
+ return this.blogFetch("GET", templateId, `/posts${qs ? `?${qs}` : ""}`);
423
+ }
424
+
425
+ async getBlogPost(templateId: string, postId: string): Promise<any> {
426
+ return this.blogFetch("GET", templateId, `/posts/${encodeURIComponent(postId)}`);
427
+ }
428
+
429
+ async createBlogPost(templateId: string, payload: Record<string, any>): Promise<any> {
430
+ return this.blogFetch("POST", templateId, "/posts", payload);
431
+ }
432
+
433
+ async updateBlogPost(templateId: string, postId: string, payload: Record<string, any>): Promise<any> {
434
+ return this.blogFetch("PATCH", templateId, `/posts/${encodeURIComponent(postId)}`, payload);
435
+ }
436
+
437
+ async deleteBlogPost(templateId: string, postId: string): Promise<any> {
438
+ return this.blogFetch("DELETE", templateId, `/posts/${encodeURIComponent(postId)}`);
439
+ }
440
+
441
+ async listBlogCategories(templateId: string): Promise<any> {
442
+ return this.blogFetch("GET", templateId, "/categories");
443
+ }
444
+
445
+ async createBlogCategory(templateId: string, payload: { name: string; description?: string; parentId?: string | null }): Promise<any> {
446
+ return this.blogFetch("POST", templateId, "/categories", payload);
447
+ }
448
+
449
+ async deleteBlogCategory(templateId: string, categoryId: string): Promise<any> {
450
+ return this.blogFetch("DELETE", templateId, `/categories/${encodeURIComponent(categoryId)}`);
451
+ }
452
+
453
+ async listBlogTags(templateId: string): Promise<any> {
454
+ return this.blogFetch("GET", templateId, "/tags");
455
+ }
456
+
457
+ async createBlogTag(templateId: string, name: string): Promise<any> {
458
+ return this.blogFetch("POST", templateId, "/tags", { name });
459
+ }
460
+
461
+ async deleteBlogTag(templateId: string, tagId: string): Promise<any> {
462
+ return this.blogFetch("DELETE", templateId, `/tags/${encodeURIComponent(tagId)}`);
463
+ }
464
+
465
+ async listBlogAuthors(templateId: string): Promise<any> {
466
+ return this.blogFetch("GET", templateId, "/authors");
467
+ }
468
+
469
+ async createBlogAuthor(templateId: string, payload: { name: string; bio?: string; avatarUrl?: string }): Promise<any> {
470
+ return this.blogFetch("POST", templateId, "/authors", payload);
471
+ }
472
+
473
+ async deleteBlogAuthor(templateId: string, authorId: string): Promise<any> {
474
+ return this.blogFetch("DELETE", templateId, `/authors/${encodeURIComponent(authorId)}`);
475
+ }
476
+
477
+ async listBlogComments(templateId: string, status?: string): Promise<any> {
478
+ return this.blogFetch("GET", templateId, `/comments${status ? `?status=${encodeURIComponent(status)}` : ""}`);
479
+ }
480
+
481
+ async moderateBlogComment(templateId: string, commentId: string, status: string): Promise<any> {
482
+ return this.blogFetch("PATCH", templateId, `/comments/${encodeURIComponent(commentId)}`, { status });
483
+ }
484
+
485
+ async deleteBlogComment(templateId: string, commentId: string): Promise<any> {
486
+ return this.blogFetch("DELETE", templateId, `/comments/${encodeURIComponent(commentId)}`);
487
+ }
358
488
  }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  ListToolsRequestSchema,
8
8
  } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { PlexoClient } from "./client/plexoClient.js";
10
+ import { BLOG_TOOL_DEFS, BLOG_TOOL_NAMES, handleBlogToolCall } from "./blogTools.js";
10
11
 
11
12
  const server = new Server(
12
13
  {
@@ -136,7 +137,7 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
136
137
  properties: {
137
138
  type: {
138
139
  type: "string",
139
- enum: ["heading", "paragraph", "text", "button", "card", "image", "menu", "social", "divider", "spacer", "form_container", "input", "textarea", "select", "carousel", "html", "icon", "table", "timer", "video"],
140
+ enum: ["heading", "paragraph", "text", "button", "card", "image", "menu", "social", "divider", "spacer", "form_container", "input", "textarea", "select", "carousel", "html", "icon", "table", "timer", "video", "accordion", "blog_title", "blog_content", "blog_featured_image", "blog_date", "blog_author", "blog_categories", "blog_comments", "blog_post_list"],
140
141
  },
141
142
  style: { type: "object" },
142
143
  attributes: { type: "object", description: "Component attributes (text, href, src, title, description, links, fields). For type 'html', raw markup goes under 'htmlContent' — NOT 'text'." },
@@ -228,7 +229,7 @@ Uses the SAME fully-hydrated layout schema as publish_landing_page — designJso
228
229
  properties: {
229
230
  type: {
230
231
  type: "string",
231
- enum: ["heading", "paragraph", "text", "button", "card", "image", "menu", "social", "divider", "spacer", "form_container", "input", "textarea", "select", "carousel", "html", "icon", "table", "timer", "video"],
232
+ enum: ["heading", "paragraph", "text", "button", "card", "image", "menu", "social", "divider", "spacer", "form_container", "input", "textarea", "select", "carousel", "html", "icon", "table", "timer", "video", "accordion", "blog_title", "blog_content", "blog_featured_image", "blog_date", "blog_author", "blog_categories", "blog_comments", "blog_post_list"],
232
233
  },
233
234
  style: { type: "object" },
234
235
  attributes: {
@@ -503,6 +504,7 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
503
504
  },
504
505
  },
505
506
  },
507
+ ...BLOG_TOOL_DEFS,
506
508
  ],
507
509
  };
508
510
  });
@@ -720,8 +722,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
720
722
  };
721
723
  }
722
724
 
723
- default:
725
+ default: {
726
+ if (BLOG_TOOL_NAMES.has(name)) {
727
+ const result = await handleBlogToolCall(name, (args as any) || {}, client);
728
+ return {
729
+ content: [
730
+ {
731
+ type: "text",
732
+ text: JSON.stringify(result, null, 2),
733
+ },
734
+ ],
735
+ };
736
+ }
724
737
  throw new Error(`Unknown tool: ${name}`);
738
+ }
725
739
  }
726
740
  } catch (error) {
727
741
  return {