@rimelight/cms 0.0.8 → 0.0.10

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/src/mcp/index.ts CHANGED
@@ -1,11 +1,21 @@
1
- import { and, desc, eq, isNotNull, isNull } from "drizzle-orm"
1
+ import { and, desc, eq, isNotNull, isNull, like, or, sql } from "drizzle-orm"
2
2
  import { pages } from "../schema/pages.ts"
3
3
  import { pageDrafts } from "../schema/page_drafts.ts"
4
+ import { pageVersions } from "../schema/page_versions.ts"
5
+ import { pageTemplates } from "../schema/page_templates.ts"
6
+ import { contentTypes } from "../schema/content_types.ts"
7
+ import { bylines } from "../schema/bylines.ts"
8
+ import { taxonomyTerms, pageTaxonomyTerms } from "../schema/taxonomies.ts"
9
+ import { pageVersionApprovals } from "../schema/page_version_approvals.ts"
10
+ import { pageVersionComments } from "../schema/page_version_comments.ts"
4
11
  import { getSiteSettings, updateSiteSettings } from "../services/site-settings.ts"
12
+ import { getTaxonomyTerms } from "../services/taxonomies.ts"
13
+ import { indexPageForSearch } from "../services/search-indexer.ts"
5
14
  import type { PageType } from "../core/types/pages.ts"
6
15
  import type { BaseBlock } from "../core/types/blocks.ts"
7
16
 
8
17
  export const CMS_MCP_TOOLS = [
18
+ // ─── 1. Pages & Drafts ──────────────────────────────────────────────────────
9
19
  {
10
20
  name: "cms_list_pages",
11
21
  description: "List CMS pages with optional filters for type, draft status, and pagination.",
@@ -14,17 +24,18 @@ export const CMS_MCP_TOOLS = [
14
24
  properties: {
15
25
  type: {
16
26
  type: "string",
17
- description: "Filter by page type (e.g. 'blog', 'docs', 'landing')"
27
+ description: "Filter by page type (e.g. 'blog', 'doc', 'legal', 'wiki')"
18
28
  },
19
29
  includeDrafts: { type: "boolean", description: "Whether to include unpublished drafts" },
20
- limit: { type: "number", description: "Maximum number of pages to return (default 20)" }
30
+ limit: { type: "number", description: "Maximum number of pages to return (default 20)" },
31
+ offset: { type: "number", description: "Offset for pagination (default 0)" }
21
32
  }
22
33
  }
23
34
  },
24
35
  {
25
36
  name: "cms_get_page",
26
37
  description:
27
- "Get full details, block tree, and properties for a specific CMS page by slug or id.",
38
+ "Get full details, block tree, properties, bylines, and active draft for a page by slug or UUID.",
28
39
  inputSchema: {
29
40
  type: "object",
30
41
  properties: {
@@ -33,20 +44,473 @@ export const CMS_MCP_TOOLS = [
33
44
  required: ["slugOrId"]
34
45
  }
35
46
  },
47
+ {
48
+ name: "cms_create_page",
49
+ description: "Create a new CMS page and its initial draft blocks.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ slug: { type: "string", description: "Unique URL slug for the page" },
54
+ type: {
55
+ type: "string",
56
+ description: "Page type (e.g. 'blog', 'doc', 'legal', 'wiki', 'character')"
57
+ },
58
+ title: { type: "object", description: "Localized title object, e.g. { en: 'Page Title' }" },
59
+ description: {
60
+ type: "object",
61
+ description: "Localized description object, e.g. { en: 'Page description' }"
62
+ },
63
+ templateId: {
64
+ type: "string",
65
+ description: "Optional UUID of the template to base the page on"
66
+ },
67
+ tags: { type: "array", description: "Array of localized tags or strings" },
68
+ authorIds: { type: "array", description: "Array of author user IDs" },
69
+ bylines: {
70
+ type: "array",
71
+ description: "Array of structured byline credits [{ bylineId, role, customCredit }]"
72
+ },
73
+ blocks: { type: "array", description: "Initial structured CMS blocks array" },
74
+ properties: { type: "object", description: "Content type specific custom properties" }
75
+ },
76
+ required: ["slug", "type", "title"]
77
+ }
78
+ },
79
+ {
80
+ name: "cms_update_page",
81
+ description: "Update an existing CMS page's metadata, properties, bylines, or content blocks.",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ id: { type: "string", description: "Page UUID" },
86
+ slug: { type: "string", description: "New unique slug" },
87
+ title: { type: "object", description: "Localized title object" },
88
+ description: { type: "object", description: "Localized description object" },
89
+ tags: { type: "array", description: "Array of localized tags" },
90
+ bylines: { type: "array", description: "Array of structured byline credits" },
91
+ blocks: { type: "array", description: "Updated structured CMS blocks array" },
92
+ properties: { type: "object", description: "Updated custom properties object" }
93
+ },
94
+ required: ["id"]
95
+ }
96
+ },
36
97
  {
37
98
  name: "cms_create_draft",
38
- description: "Create or update a draft version for a CMS page.",
99
+ description: "Create or update an in-progress draft version for a CMS page without publishing.",
39
100
  inputSchema: {
40
101
  type: "object",
41
102
  properties: {
42
103
  pageId: { type: "string", description: "Page UUID" },
43
- userId: { type: "string", description: "User ID creating the draft" },
104
+ userId: { type: "string", description: "User or Agent ID creating the draft" },
44
105
  blocks: { type: "array", description: "Array of structured CMS blocks" },
45
106
  properties: { type: "object", description: "Page properties object" }
46
107
  },
47
108
  required: ["pageId", "userId", "blocks"]
48
109
  }
49
110
  },
111
+ {
112
+ name: "cms_publish_page",
113
+ description: "Publish a page draft, creating a new immutable version snapshot.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ pageId: { type: "string", description: "Page UUID" },
118
+ userId: { type: "string", description: "User or Agent ID publishing the page" },
119
+ changeSummary: { type: "string", description: "Summary of changes in this version" }
120
+ },
121
+ required: ["pageId", "userId"]
122
+ }
123
+ },
124
+ {
125
+ name: "cms_unpublish_page",
126
+ description: "Unpublish a live page, reverting it to draft status.",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ pageId: { type: "string", description: "Page UUID to unpublish" }
131
+ },
132
+ required: ["pageId"]
133
+ }
134
+ },
135
+ {
136
+ name: "cms_delete_page",
137
+ description: "Soft delete a CMS page by setting deleted_at.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ pageId: { type: "string", description: "Page UUID to delete" }
142
+ },
143
+ required: ["pageId"]
144
+ }
145
+ },
146
+
147
+ // ─── 2. Page Versions & Approvals ───────────────────────────────────────────
148
+ {
149
+ name: "cms_list_page_versions",
150
+ description: "List version history snapshots for a page.",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ pageId: { type: "string", description: "Page UUID" },
155
+ limit: { type: "number", description: "Max versions to return (default 20)" }
156
+ },
157
+ required: ["pageId"]
158
+ }
159
+ },
160
+ {
161
+ name: "cms_get_page_version",
162
+ description: "Get full snapshot content and block tree for a specific version.",
163
+ inputSchema: {
164
+ type: "object",
165
+ properties: {
166
+ versionId: { type: "string", description: "Version UUID" }
167
+ },
168
+ required: ["versionId"]
169
+ }
170
+ },
171
+ {
172
+ name: "cms_rollback_page_version",
173
+ description: "Rollback a page's working draft or live content to a past version snapshot.",
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ pageId: { type: "string", description: "Page UUID" },
178
+ versionId: { type: "string", description: "Target Version UUID to rollback to" },
179
+ userId: { type: "string", description: "User or Agent ID performing rollback" }
180
+ },
181
+ required: ["pageId", "versionId", "userId"]
182
+ }
183
+ },
184
+ {
185
+ name: "cms_approve_page_version",
186
+ description: "Record an editorial review approval for a version.",
187
+ inputSchema: {
188
+ type: "object",
189
+ properties: {
190
+ versionId: { type: "string", description: "Version UUID" },
191
+ userId: { type: "string", description: "Approver user or agent ID" },
192
+ userRole: { type: "string", description: "Approver role, e.g. 'Editor', 'Admin'" }
193
+ },
194
+ required: ["versionId", "userId", "userRole"]
195
+ }
196
+ },
197
+ {
198
+ name: "cms_add_version_comment",
199
+ description: "Add an editorial or block-level comment to a version.",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: {
203
+ versionId: { type: "string", description: "Version UUID" },
204
+ userId: { type: "string", description: "Author user or agent ID" },
205
+ userRole: { type: "string", description: "Author role" },
206
+ content: { type: "string", description: "Comment body text" },
207
+ blockId: { type: "string", description: "Optional specific block ID target" }
208
+ },
209
+ required: ["versionId", "userId", "userRole", "content"]
210
+ }
211
+ },
212
+
213
+ // ─── 3. Templates ───────────────────────────────────────────────────────────
214
+ {
215
+ name: "cms_list_templates",
216
+ description: "List all page templates with starter blocks and property schemas.",
217
+ inputSchema: {
218
+ type: "object",
219
+ properties: {
220
+ pageType: { type: "string", description: "Optional filter by page type" }
221
+ }
222
+ }
223
+ },
224
+ {
225
+ name: "cms_get_template",
226
+ description: "Get template definition by slug or UUID.",
227
+ inputSchema: {
228
+ type: "object",
229
+ properties: {
230
+ slugOrId: { type: "string", description: "Template slug or UUID" }
231
+ },
232
+ required: ["slugOrId"]
233
+ }
234
+ },
235
+ {
236
+ name: "cms_create_template",
237
+ description: "Create a new page template with default property groups and starter blocks.",
238
+ inputSchema: {
239
+ type: "object",
240
+ properties: {
241
+ slug: { type: "string", description: "Unique template slug" },
242
+ title: { type: "object", description: "Localized title, e.g. { en: 'Wiki Template' }" },
243
+ description: { type: "object", description: "Localized description" },
244
+ pageType: { type: "string", description: "Page type target" },
245
+ version: { type: "number", description: "Template version number (default 1)" },
246
+ allowedRoles: { type: "array", description: "Roles allowed to use this template" },
247
+ rolePermissions: { type: "object", description: "Granular template role permissions" },
248
+ approvalRules: { type: "object", description: "Approval workflow configuration" },
249
+ defaultProperties: { type: "object", description: "Default property schema groups" },
250
+ initialBlocks: { type: "array", description: "Starter structured CMS blocks" }
251
+ },
252
+ required: ["slug", "title", "pageType"]
253
+ }
254
+ },
255
+ {
256
+ name: "cms_update_template",
257
+ description: "Update an existing page template.",
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {
261
+ id: { type: "string", description: "Template UUID" },
262
+ slug: { type: "string" },
263
+ title: { type: "object" },
264
+ description: { type: "object" },
265
+ pageType: { type: "string" },
266
+ version: { type: "number" },
267
+ defaultProperties: { type: "object" },
268
+ initialBlocks: { type: "array" },
269
+ rolePermissions: { type: "object" },
270
+ approvalRules: { type: "object" }
271
+ },
272
+ required: ["id"]
273
+ }
274
+ },
275
+ {
276
+ name: "cms_delete_template",
277
+ description: "Delete a page template by UUID.",
278
+ inputSchema: {
279
+ type: "object",
280
+ properties: {
281
+ id: { type: "string", description: "Template UUID" }
282
+ },
283
+ required: ["id"]
284
+ }
285
+ },
286
+
287
+ // ─── 4. Taxonomies (Categories & Tags) ──────────────────────────────────────
288
+ {
289
+ name: "cms_list_taxonomies",
290
+ description: "List taxonomy terms (categories, tags, genres, etc.) with usage counts.",
291
+ inputSchema: {
292
+ type: "object",
293
+ properties: {
294
+ taxonomy: { type: "string", description: "Taxonomy type, e.g. 'category', 'tag'" }
295
+ },
296
+ required: ["taxonomy"]
297
+ }
298
+ },
299
+ {
300
+ name: "cms_create_taxonomy_term",
301
+ description: "Create a new taxonomy term (category or tag).",
302
+ inputSchema: {
303
+ type: "object",
304
+ properties: {
305
+ taxonomy: { type: "string", description: "'category', 'tag', or custom taxonomy name" },
306
+ slug: { type: "string", description: "Unique slug within taxonomy" },
307
+ label: { type: "object", description: "Localized label or string, e.g. { en: 'Guides' }" },
308
+ description: { type: "object", description: "Localized description" },
309
+ parentId: { type: "string", description: "Optional parent category UUID for hierarchy" },
310
+ displayOrder: { type: "number", description: "Display sort order" }
311
+ },
312
+ required: ["taxonomy", "slug", "label"]
313
+ }
314
+ },
315
+ {
316
+ name: "cms_update_taxonomy_term",
317
+ description: "Update an existing taxonomy term by UUID.",
318
+ inputSchema: {
319
+ type: "object",
320
+ properties: {
321
+ id: { type: "string", description: "Taxonomy term UUID" },
322
+ slug: { type: "string" },
323
+ label: { type: "object" },
324
+ description: { type: "object" },
325
+ parentId: { type: "string" },
326
+ displayOrder: { type: "number" }
327
+ },
328
+ required: ["id"]
329
+ }
330
+ },
331
+ {
332
+ name: "cms_delete_taxonomy_term",
333
+ description: "Delete a taxonomy term by UUID.",
334
+ inputSchema: {
335
+ type: "object",
336
+ properties: {
337
+ id: { type: "string", description: "Taxonomy term UUID" }
338
+ },
339
+ required: ["id"]
340
+ }
341
+ },
342
+ {
343
+ name: "cms_assign_page_taxonomy",
344
+ description: "Assign or unassign taxonomy terms to/from a page.",
345
+ inputSchema: {
346
+ type: "object",
347
+ properties: {
348
+ pageId: { type: "string", description: "Page UUID" },
349
+ termIds: {
350
+ type: "array",
351
+ description: "Array of taxonomy term UUIDs to assign to the page"
352
+ }
353
+ },
354
+ required: ["pageId", "termIds"]
355
+ }
356
+ },
357
+
358
+ // ─── 5. Bylines & Author Schemas ────────────────────────────────────────────
359
+ {
360
+ name: "cms_list_bylines",
361
+ description: "List all registered author and contributor bylines.",
362
+ inputSchema: {
363
+ type: "object",
364
+ properties: {}
365
+ }
366
+ },
367
+ {
368
+ name: "cms_get_byline",
369
+ description: "Get a specific byline profile by slug or UUID.",
370
+ inputSchema: {
371
+ type: "object",
372
+ properties: {
373
+ slugOrId: { type: "string", description: "Byline slug or UUID" }
374
+ },
375
+ required: ["slugOrId"]
376
+ }
377
+ },
378
+ {
379
+ name: "cms_create_byline",
380
+ description: "Create an author, creator, or studio byline profile.",
381
+ inputSchema: {
382
+ type: "object",
383
+ properties: {
384
+ name: { type: "string", description: "Display name" },
385
+ slug: { type: "string", description: "Unique URL slug" },
386
+ websiteUrl: { type: "string", description: "Author website URL" },
387
+ bio: { type: "string", description: "Bio or summary text" },
388
+ avatar: { type: "string", description: "Avatar image URL or asset key" },
389
+ userId: { type: "string", description: "Linked user ID if applicable" },
390
+ socials: {
391
+ type: "object",
392
+ description: "Social links, e.g. { twitter: '...', github: '...' }"
393
+ },
394
+ metadata: { type: "object", description: "Custom author metadata" }
395
+ },
396
+ required: ["name", "slug"]
397
+ }
398
+ },
399
+ {
400
+ name: "cms_update_byline",
401
+ description: "Update an existing byline profile.",
402
+ inputSchema: {
403
+ type: "object",
404
+ properties: {
405
+ id: { type: "string", description: "Byline UUID" },
406
+ name: { type: "string" },
407
+ slug: { type: "string" },
408
+ websiteUrl: { type: "string" },
409
+ bio: { type: "string" },
410
+ avatar: { type: "string" },
411
+ userId: { type: "string" },
412
+ socials: { type: "object" },
413
+ metadata: { type: "object" }
414
+ },
415
+ required: ["id"]
416
+ }
417
+ },
418
+ {
419
+ name: "cms_delete_byline",
420
+ description: "Delete an author byline profile by UUID.",
421
+ inputSchema: {
422
+ type: "object",
423
+ properties: {
424
+ id: { type: "string", description: "Byline UUID" }
425
+ },
426
+ required: ["id"]
427
+ }
428
+ },
429
+
430
+ // ─── 6. Dynamic Content Types & E-Commerce Collections ──────────────────────
431
+ {
432
+ name: "cms_list_content_types",
433
+ description: "List all dynamic content types, collections, and e-commerce models.",
434
+ inputSchema: {
435
+ type: "object",
436
+ properties: {}
437
+ }
438
+ },
439
+ {
440
+ name: "cms_get_content_type",
441
+ description: "Get dynamic content type definition by slug or UUID.",
442
+ inputSchema: {
443
+ type: "object",
444
+ properties: {
445
+ slugOrId: { type: "string", description: "Content type slug or UUID" }
446
+ },
447
+ required: ["slugOrId"]
448
+ }
449
+ },
450
+ {
451
+ name: "cms_create_content_type",
452
+ description: "Create a new dynamic collection, content type, or e-commerce schema.",
453
+ inputSchema: {
454
+ type: "object",
455
+ properties: {
456
+ slug: {
457
+ type: "string",
458
+ description: "Unique slug identifier (e.g. 'products', 'characters')"
459
+ },
460
+ name: { type: "object", description: "Localized name, e.g. { en: 'Products' }" },
461
+ description: { type: "object", description: "Localized description" },
462
+ icon: { type: "string", description: "Icon name (e.g. 'i-lucide-shopping-bag')" },
463
+ mode: {
464
+ type: "string",
465
+ description: "'document' (blocks), 'data' (structured table), or 'singleton'"
466
+ },
467
+ fieldSchema: {
468
+ type: "object",
469
+ description: "Dynamic property field schemas and validation rules"
470
+ },
471
+ ecomSettings: {
472
+ type: "object",
473
+ description:
474
+ "E-commerce settings { enabled, currencyDefault, trackInventory, hasVariants, allowDigitalDownloads, stripeSync }"
475
+ },
476
+ initialBlocks: { type: "array", description: "Starter blocks for document mode" },
477
+ isSystem: { type: "boolean", description: "Whether this is a protected system schema" }
478
+ },
479
+ required: ["slug", "name"]
480
+ }
481
+ },
482
+ {
483
+ name: "cms_update_content_type",
484
+ description: "Update an existing dynamic content type schema or e-commerce settings.",
485
+ inputSchema: {
486
+ type: "object",
487
+ properties: {
488
+ id: { type: "string", description: "Content type UUID" },
489
+ slug: { type: "string" },
490
+ name: { type: "object" },
491
+ description: { type: "object" },
492
+ icon: { type: "string" },
493
+ mode: { type: "string" },
494
+ fieldSchema: { type: "object" },
495
+ ecomSettings: { type: "object" },
496
+ initialBlocks: { type: "array" }
497
+ },
498
+ required: ["id"]
499
+ }
500
+ },
501
+ {
502
+ name: "cms_delete_content_type",
503
+ description: "Delete a custom dynamic content type by UUID.",
504
+ inputSchema: {
505
+ type: "object",
506
+ properties: {
507
+ id: { type: "string", description: "Content type UUID" }
508
+ },
509
+ required: ["id"]
510
+ }
511
+ },
512
+
513
+ // ─── 7. Site Settings ───────────────────────────────────────────────────────
50
514
  {
51
515
  name: "cms_get_site_settings",
52
516
  description: "Retrieve current dynamic site settings including branding and SEO defaults.",
@@ -69,6 +533,21 @@ export const CMS_MCP_TOOLS = [
69
533
  seo: { type: "object" }
70
534
  }
71
535
  }
536
+ },
537
+
538
+ // ─── 8. Search ──────────────────────────────────────────────────────────────
539
+ {
540
+ name: "cms_search_content",
541
+ description: "Search across active CMS pages by keyword.",
542
+ inputSchema: {
543
+ type: "object",
544
+ properties: {
545
+ query: { type: "string", description: "Search query text" },
546
+ type: { type: "string", description: "Optional filter by page type" },
547
+ limit: { type: "number", description: "Max results to return (default 10)" }
548
+ },
549
+ required: ["query"]
550
+ }
72
551
  }
73
552
  ]
74
553
 
@@ -100,10 +579,10 @@ export async function handleCmsMcpRequest(
100
579
  const { name, arguments: args = {} } = params
101
580
 
102
581
  switch (name) {
582
+ // ─── Pages ───────────────────────────────────────────────────────────────
103
583
  case "cms_list_pages": {
104
584
  const conditions = [isNull(pages.deletedAt)]
105
585
  if (args.type) {
106
- // eslint-disable-next-line typescript/no-unsafe-type-assertion
107
586
  conditions.push(eq(pages.type, args.type as PageType))
108
587
  }
109
588
  if (!args.includeDrafts) {
@@ -111,19 +590,24 @@ export async function handleCmsMcpRequest(
111
590
  }
112
591
 
113
592
  const queryLimit = typeof args.limit === "number" ? args.limit : 20
593
+ const queryOffset = typeof args.offset === "number" ? args.offset : 0
114
594
  const rows = await db
115
595
  .select({
116
596
  id: pages.id,
117
597
  slug: pages.slug,
118
598
  type: pages.type,
119
599
  title: pages.title,
600
+ description: pages.description,
120
601
  postedAt: pages.postedAt,
121
- publishedVersionId: pages.publishedVersionId
602
+ publishedVersionId: pages.publishedVersionId,
603
+ createdAt: pages.createdAt,
604
+ updatedAt: pages.updatedAt
122
605
  })
123
606
  .from(pages)
124
607
  .where(and(...conditions))
125
- .orderBy(desc(pages.postedAt))
608
+ .orderBy(desc(pages.postedAt), desc(pages.createdAt))
126
609
  .limit(queryLimit)
610
+ .offset(queryOffset)
127
611
 
128
612
  return {
129
613
  jsonrpc: "2.0",
@@ -135,25 +619,124 @@ export async function handleCmsMcpRequest(
135
619
  }
136
620
 
137
621
  case "cms_get_page": {
622
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
623
+ args.slugOrId
624
+ )
625
+ const condition = isUuid ? eq(pages.id, args.slugOrId) : eq(pages.slug, args.slugOrId)
626
+
138
627
  const row = await db
139
628
  .select()
140
629
  .from(pages)
141
- .where(and(isNull(pages.deletedAt), eq(pages.slug, args.slugOrId)))
630
+ .where(and(isNull(pages.deletedAt), condition))
142
631
  .limit(1)
143
-
144
632
  const page = row[0] || null
633
+ if (!page) {
634
+ return {
635
+ jsonrpc: "2.0",
636
+ id,
637
+ error: { code: -32004, message: `Page '${args.slugOrId}' not found` }
638
+ }
639
+ }
640
+
641
+ let draft = null
642
+ try {
643
+ const draftRow = await db
644
+ .select()
645
+ .from(pageDrafts)
646
+ .where(eq(pageDrafts.pageId, page.id))
647
+ .limit(1)
648
+ draft = draftRow[0] || null
649
+ } catch {}
650
+
145
651
  return {
146
652
  jsonrpc: "2.0",
147
653
  id,
148
- result: {
149
- content: [{ type: "text", text: JSON.stringify(page, null, 2) }]
150
- }
654
+ result: { content: [{ type: "text", text: JSON.stringify({ page, draft }, null, 2) }] }
151
655
  }
152
656
  }
153
657
 
154
- case "cms_create_draft": {
155
- const contentPayload = {
156
- blocks: args.blocks as BaseBlock[],
658
+ case "cms_create_page": {
659
+ const titlePayload = typeof args.title === "string" ? { en: args.title } : args.title
660
+ const descPayload =
661
+ typeof args.description === "string" ? { en: args.description } : args.description || null
662
+ const blocksPayload = Array.isArray(args.blocks) ? args.blocks : []
663
+ const propertiesPayload = args.properties || {}
664
+
665
+ const inserted = await db
666
+ .insert(pages)
667
+ .values({
668
+ slug: args.slug,
669
+ type: args.type as PageType,
670
+ templateId: args.templateId || null,
671
+ title: titlePayload,
672
+ description: descPayload,
673
+ tags: args.tags || [],
674
+ authorIds: args.authorIds || [],
675
+ bylines: args.bylines || [],
676
+ content: {
677
+ blocks: blocksPayload,
678
+ properties: propertiesPayload
679
+ }
680
+ })
681
+ .returning()
682
+
683
+ const newPage = inserted[0]
684
+ if (newPage) {
685
+ try {
686
+ await db
687
+ .insert(pageDrafts)
688
+ .values({
689
+ pageId: newPage.id,
690
+ updatedBy: args.authorIds?.[0] || "agent",
691
+ content: { blocks: blocksPayload, properties: propertiesPayload }
692
+ })
693
+ .onConflictDoNothing()
694
+ } catch {}
695
+ }
696
+
697
+ return {
698
+ jsonrpc: "2.0",
699
+ id,
700
+ result: { content: [{ type: "text", text: JSON.stringify(newPage, null, 2) }] }
701
+ }
702
+ }
703
+
704
+ case "cms_update_page": {
705
+ const updateData: Record<string, any> = { updatedAt: new Date() }
706
+ if (args.slug) updateData["slug"] = args.slug
707
+ if (args.title)
708
+ updateData["title"] = typeof args.title === "string" ? { en: args.title } : args.title
709
+ if (args.description !== undefined) {
710
+ updateData["description"] =
711
+ typeof args.description === "string" ? { en: args.description } : args.description
712
+ }
713
+ if (args.tags) updateData["tags"] = args.tags
714
+ if (args.bylines) updateData["bylines"] = args.bylines
715
+
716
+ if (args.blocks || args.properties) {
717
+ const current = await db.select().from(pages).where(eq(pages.id, args.id)).limit(1)
718
+ const existingContent = current[0]?.content || { blocks: [], properties: {} }
719
+ updateData["content"] = {
720
+ blocks: args.blocks || existingContent.blocks || [],
721
+ properties: args.properties || existingContent.properties || {}
722
+ }
723
+ }
724
+
725
+ const updated = await db
726
+ .update(pages)
727
+ .set(updateData)
728
+ .where(eq(pages.id, args.id))
729
+ .returning()
730
+ return {
731
+ jsonrpc: "2.0",
732
+ id,
733
+ result: { content: [{ type: "text", text: JSON.stringify(updated[0] || null, null, 2) }] }
734
+ }
735
+ }
736
+
737
+ case "cms_create_draft": {
738
+ const contentPayload = {
739
+ blocks: args.blocks as BaseBlock[],
157
740
  properties: args.properties || {}
158
741
  }
159
742
 
@@ -174,55 +757,730 @@ export async function handleCmsMcpRequest(
174
757
  })
175
758
  .returning()
176
759
 
760
+ return {
761
+ jsonrpc: "2.0",
762
+ id,
763
+ result: { content: [{ type: "text", text: JSON.stringify(draft[0], null, 2) }] }
764
+ }
765
+ }
766
+
767
+ case "cms_publish_page": {
768
+ const pageResult = await db.select().from(pages).where(eq(pages.id, args.pageId)).limit(1)
769
+ const page = pageResult[0]
770
+ if (!page) {
771
+ return {
772
+ jsonrpc: "2.0",
773
+ id,
774
+ error: { code: -32004, message: `Page '${args.pageId}' not found` }
775
+ }
776
+ }
777
+
778
+ const draftResult = await db
779
+ .select()
780
+ .from(pageDrafts)
781
+ .where(eq(pageDrafts.pageId, args.pageId))
782
+ .limit(1)
783
+ const content = draftResult[0]?.content || page.content
784
+
785
+ const lastVersion = await db
786
+ .select()
787
+ .from(pageVersions)
788
+ .where(eq(pageVersions.pageId, args.pageId))
789
+ .orderBy(desc(pageVersions.versionNumber))
790
+ .limit(1)
791
+
792
+ const nextVersionNumber = (lastVersion[0]?.versionNumber || 0) + 1
793
+
794
+ const versionResult = await db
795
+ .insert(pageVersions)
796
+ .values({
797
+ pageId: args.pageId,
798
+ versionNumber: nextVersionNumber,
799
+ status: "published",
800
+ slug: page.slug,
801
+ type: page.type,
802
+ title: page.title,
803
+ description: page.description,
804
+ tags: page.tags,
805
+ authorIds: page.authorIds,
806
+ bylines: page.bylines,
807
+ content,
808
+ createdBy: args.userId,
809
+ approvedBy: [args.userId],
810
+ approvedAt: new Date(),
811
+ changeSummary: args.changeSummary || "Published via MCP",
812
+ createdAt: new Date()
813
+ })
814
+ .returning()
815
+
816
+ const version = versionResult[0]
817
+
818
+ await db
819
+ .update(pages)
820
+ .set({
821
+ content,
822
+ publishedVersionId: version.id,
823
+ postedAt: new Date(),
824
+ updatedAt: new Date()
825
+ })
826
+ .where(eq(pages.id, args.pageId))
827
+
828
+ await indexPageForSearch(db, { id: args.pageId, title: page.title, content }, ["en", "pt"])
829
+
830
+ return {
831
+ jsonrpc: "2.0",
832
+ id,
833
+ result: { content: [{ type: "text", text: JSON.stringify(version, null, 2) }] }
834
+ }
835
+ }
836
+
837
+ case "cms_unpublish_page": {
838
+ const updated = await db
839
+ .update(pages)
840
+ .set({ postedAt: null, publishedVersionId: null, updatedAt: new Date() })
841
+ .where(eq(pages.id, args.pageId))
842
+ .returning()
843
+
177
844
  return {
178
845
  jsonrpc: "2.0",
179
846
  id,
180
847
  result: {
181
- content: [{ type: "text", text: JSON.stringify(draft[0], null, 2) }]
848
+ content: [
849
+ {
850
+ type: "text",
851
+ text: JSON.stringify({ success: true, page: updated[0] || null }, null, 2)
852
+ }
853
+ ]
182
854
  }
183
855
  }
184
856
  }
185
857
 
186
- case "cms_get_site_settings": {
187
- const settings = await getSiteSettings(db)
858
+ case "cms_delete_page": {
859
+ const deleted = await db
860
+ .update(pages)
861
+ .set({ deletedAt: new Date() })
862
+ .where(eq(pages.id, args.pageId))
863
+ .returning()
864
+
188
865
  return {
189
866
  jsonrpc: "2.0",
190
867
  id,
191
868
  result: {
192
- content: [{ type: "text", text: JSON.stringify(settings, null, 2) }]
869
+ content: [
870
+ {
871
+ type: "text",
872
+ text: JSON.stringify({ success: true, page: deleted[0] || null }, null, 2)
873
+ }
874
+ ]
193
875
  }
194
876
  }
195
877
  }
196
878
 
197
- case "cms_update_site_settings": {
198
- const updated = await updateSiteSettings(db, args)
879
+ // ─── Versions & Reviews ───────────────────────────────────────────────────
880
+ case "cms_list_page_versions": {
881
+ const vLimit = typeof args.limit === "number" ? args.limit : 20
882
+ const rows = await db
883
+ .select()
884
+ .from(pageVersions)
885
+ .where(eq(pageVersions.pageId, args.pageId))
886
+ .orderBy(desc(pageVersions.versionNumber))
887
+ .limit(vLimit)
888
+
889
+ return {
890
+ jsonrpc: "2.0",
891
+ id,
892
+ result: { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] }
893
+ }
894
+ }
895
+
896
+ case "cms_get_page_version": {
897
+ const row = await db
898
+ .select()
899
+ .from(pageVersions)
900
+ .where(eq(pageVersions.id, args.versionId))
901
+ .limit(1)
902
+ return {
903
+ jsonrpc: "2.0",
904
+ id,
905
+ result: { content: [{ type: "text", text: JSON.stringify(row[0] || null, null, 2) }] }
906
+ }
907
+ }
908
+
909
+ case "cms_rollback_page_version": {
910
+ const vRow = await db
911
+ .select()
912
+ .from(pageVersions)
913
+ .where(eq(pageVersions.id, args.versionId))
914
+ .limit(1)
915
+ const targetVersion = vRow[0]
916
+ if (!targetVersion) {
917
+ return {
918
+ jsonrpc: "2.0",
919
+ id,
920
+ error: { code: -32004, message: "Version snapshot not found" }
921
+ }
922
+ }
923
+
924
+ const draft = await db
925
+ .insert(pageDrafts)
926
+ .values({
927
+ pageId: args.pageId,
928
+ updatedBy: args.userId,
929
+ content: targetVersion.content
930
+ })
931
+ .onConflictDoUpdate({
932
+ target: [pageDrafts.pageId],
933
+ set: {
934
+ content: targetVersion.content,
935
+ updatedBy: args.userId,
936
+ updatedAt: new Date()
937
+ }
938
+ })
939
+ .returning()
940
+
199
941
  return {
200
942
  jsonrpc: "2.0",
201
943
  id,
202
944
  result: {
203
- content: [{ type: "text", text: JSON.stringify(updated, null, 2) }]
945
+ content: [
946
+ {
947
+ type: "text",
948
+ text: JSON.stringify(
949
+ {
950
+ success: true,
951
+ rolledBackToVersion: targetVersion.versionNumber,
952
+ draft: draft[0]
953
+ },
954
+ null,
955
+ 2
956
+ )
957
+ }
958
+ ]
204
959
  }
205
960
  }
206
961
  }
207
962
 
208
- default:
963
+ case "cms_approve_page_version": {
964
+ const approval = await db
965
+ .insert(pageVersionApprovals)
966
+ .values({
967
+ versionId: args.versionId,
968
+ userId: args.userId,
969
+ userRole: args.userRole,
970
+ approvedAt: new Date()
971
+ })
972
+ .returning()
973
+
974
+ return {
975
+ jsonrpc: "2.0",
976
+ id,
977
+ result: { content: [{ type: "text", text: JSON.stringify(approval[0], null, 2) }] }
978
+ }
979
+ }
980
+
981
+ case "cms_add_version_comment": {
982
+ const comment = await db
983
+ .insert(pageVersionComments)
984
+ .values({
985
+ versionId: args.versionId,
986
+ userId: args.userId,
987
+ userRole: args.userRole,
988
+ content: args.content,
989
+ blockId: args.blockId || null,
990
+ createdAt: new Date()
991
+ })
992
+ .returning()
993
+
994
+ return {
995
+ jsonrpc: "2.0",
996
+ id,
997
+ result: { content: [{ type: "text", text: JSON.stringify(comment[0], null, 2) }] }
998
+ }
999
+ }
1000
+
1001
+ // ─── Templates ───────────────────────────────────────────────────────────
1002
+ case "cms_list_templates": {
1003
+ const conditions = []
1004
+ if (args.pageType) {
1005
+ conditions.push(eq(pageTemplates.pageType, args.pageType))
1006
+ }
1007
+ const rows = await db
1008
+ .select()
1009
+ .from(pageTemplates)
1010
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
1011
+ .orderBy(desc(pageTemplates.createdAt))
1012
+
1013
+ return {
1014
+ jsonrpc: "2.0",
1015
+ id,
1016
+ result: { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] }
1017
+ }
1018
+ }
1019
+
1020
+ case "cms_get_template": {
1021
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
1022
+ args.slugOrId
1023
+ )
1024
+ const condition = isUuid
1025
+ ? eq(pageTemplates.id, args.slugOrId)
1026
+ : eq(pageTemplates.slug, args.slugOrId)
1027
+ const row = await db.select().from(pageTemplates).where(condition).limit(1)
1028
+
1029
+ return {
1030
+ jsonrpc: "2.0",
1031
+ id,
1032
+ result: { content: [{ type: "text", text: JSON.stringify(row[0] || null, null, 2) }] }
1033
+ }
1034
+ }
1035
+
1036
+ case "cms_create_template": {
1037
+ const titlePayload = typeof args.title === "string" ? { en: args.title } : args.title
1038
+ const descPayload =
1039
+ typeof args.description === "string" ? { en: args.description } : args.description || null
1040
+
1041
+ const tmpl = await db
1042
+ .insert(pageTemplates)
1043
+ .values({
1044
+ slug: args.slug,
1045
+ title: titlePayload,
1046
+ description: descPayload,
1047
+ pageType: args.pageType,
1048
+ version: args.version || 1,
1049
+ allowedRoles: args.allowedRoles || [],
1050
+ rolePermissions: args.rolePermissions || {
1051
+ whoCanCreate: [],
1052
+ whoCanEdit: [],
1053
+ whoCanReview: [],
1054
+ whoCanView: []
1055
+ },
1056
+ approvalRules: args.approvalRules || { allowSelfApproval: true, minApprovals: 1 },
1057
+ defaultProperties: args.defaultProperties || {},
1058
+ initialBlocks: args.initialBlocks || []
1059
+ })
1060
+ .returning()
1061
+
1062
+ return {
1063
+ jsonrpc: "2.0",
1064
+ id,
1065
+ result: { content: [{ type: "text", text: JSON.stringify(tmpl[0], null, 2) }] }
1066
+ }
1067
+ }
1068
+
1069
+ case "cms_update_template": {
1070
+ const updateData: Record<string, any> = { updatedAt: new Date() }
1071
+ if (args.slug) updateData["slug"] = args.slug
1072
+ if (args.title)
1073
+ updateData["title"] = typeof args.title === "string" ? { en: args.title } : args.title
1074
+ if (args.description !== undefined) {
1075
+ updateData["description"] =
1076
+ typeof args.description === "string" ? { en: args.description } : args.description
1077
+ }
1078
+ if (args.pageType) updateData["pageType"] = args.pageType
1079
+ if (args.version) updateData["version"] = args.version
1080
+ if (args.defaultProperties) updateData["defaultProperties"] = args.defaultProperties
1081
+ if (args.initialBlocks) updateData["initialBlocks"] = args.initialBlocks
1082
+ if (args.rolePermissions) updateData["rolePermissions"] = args.rolePermissions
1083
+ if (args.approvalRules) updateData["approvalRules"] = args.approvalRules
1084
+
1085
+ const updated = await db
1086
+ .update(pageTemplates)
1087
+ .set(updateData)
1088
+ .where(eq(pageTemplates.id, args.id))
1089
+ .returning()
1090
+ return {
1091
+ jsonrpc: "2.0",
1092
+ id,
1093
+ result: { content: [{ type: "text", text: JSON.stringify(updated[0] || null, null, 2) }] }
1094
+ }
1095
+ }
1096
+
1097
+ case "cms_delete_template": {
1098
+ const deleted = await db
1099
+ .delete(pageTemplates)
1100
+ .where(eq(pageTemplates.id, args.id))
1101
+ .returning()
1102
+ return {
1103
+ jsonrpc: "2.0",
1104
+ id,
1105
+ result: {
1106
+ content: [
1107
+ {
1108
+ type: "text",
1109
+ text: JSON.stringify({ success: true, template: deleted[0] || null }, null, 2)
1110
+ }
1111
+ ]
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ // ─── Taxonomies ───────────────────────────────────────────────────────────
1117
+ case "cms_list_taxonomies": {
1118
+ const terms = await getTaxonomyTerms(db, args.taxonomy)
209
1119
  return {
210
1120
  jsonrpc: "2.0",
211
1121
  id,
212
- error: {
213
- code: -32601,
214
- message: `Method or tool '${name}' not found`
1122
+ result: { content: [{ type: "text", text: JSON.stringify(terms, null, 2) }] }
1123
+ }
1124
+ }
1125
+
1126
+ case "cms_create_taxonomy_term": {
1127
+ const labelPayload = typeof args.label === "string" ? { en: args.label } : args.label
1128
+ const descPayload =
1129
+ typeof args.description === "string" ? { en: args.description } : args.description || null
1130
+
1131
+ const term = await db
1132
+ .insert(taxonomyTerms)
1133
+ .values({
1134
+ taxonomy: args.taxonomy,
1135
+ slug: args.slug,
1136
+ label: labelPayload,
1137
+ description: descPayload,
1138
+ parentId: args.parentId || null,
1139
+ displayOrder: args.displayOrder || 0
1140
+ })
1141
+ .returning()
1142
+
1143
+ return {
1144
+ jsonrpc: "2.0",
1145
+ id,
1146
+ result: { content: [{ type: "text", text: JSON.stringify(term[0], null, 2) }] }
1147
+ }
1148
+ }
1149
+
1150
+ case "cms_update_taxonomy_term": {
1151
+ const updateData: Record<string, any> = { updatedAt: new Date() }
1152
+ if (args.slug) updateData["slug"] = args.slug
1153
+ if (args.label)
1154
+ updateData["label"] = typeof args.label === "string" ? { en: args.label } : args.label
1155
+ if (args.description !== undefined) {
1156
+ updateData["description"] =
1157
+ typeof args.description === "string" ? { en: args.description } : args.description
1158
+ }
1159
+ if (args.parentId !== undefined) updateData["parentId"] = args.parentId || null
1160
+ if (args.displayOrder !== undefined) updateData["displayOrder"] = args.displayOrder
1161
+
1162
+ const updated = await db
1163
+ .update(taxonomyTerms)
1164
+ .set(updateData)
1165
+ .where(eq(taxonomyTerms.id, args.id))
1166
+ .returning()
1167
+ return {
1168
+ jsonrpc: "2.0",
1169
+ id,
1170
+ result: { content: [{ type: "text", text: JSON.stringify(updated[0] || null, null, 2) }] }
1171
+ }
1172
+ }
1173
+
1174
+ case "cms_delete_taxonomy_term": {
1175
+ const deleted = await db
1176
+ .delete(taxonomyTerms)
1177
+ .where(eq(taxonomyTerms.id, args.id))
1178
+ .returning()
1179
+ return {
1180
+ jsonrpc: "2.0",
1181
+ id,
1182
+ result: {
1183
+ content: [
1184
+ {
1185
+ type: "text",
1186
+ text: JSON.stringify({ success: true, term: deleted[0] || null }, null, 2)
1187
+ }
1188
+ ]
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ case "cms_assign_page_taxonomy": {
1194
+ // Clear current assignments and re-assign
1195
+ await db.delete(pageTaxonomyTerms).where(eq(pageTaxonomyTerms.pageId, args.pageId))
1196
+ const termIds = Array.isArray(args.termIds) ? args.termIds : []
1197
+ const assigned = []
1198
+ for (const termId of termIds) {
1199
+ const row = await db
1200
+ .insert(pageTaxonomyTerms)
1201
+ .values({ pageId: args.pageId, termId })
1202
+ .returning()
1203
+ assigned.push(row[0])
1204
+ }
1205
+
1206
+ return {
1207
+ jsonrpc: "2.0",
1208
+ id,
1209
+ result: {
1210
+ content: [
1211
+ {
1212
+ type: "text",
1213
+ text: JSON.stringify({ success: true, assignedCount: assigned.length }, null, 2)
1214
+ }
1215
+ ]
215
1216
  }
216
1217
  }
1218
+ }
1219
+
1220
+ // ─── Bylines ─────────────────────────────────────────────────────────────
1221
+ case "cms_list_bylines": {
1222
+ const rows = await db
1223
+ .select()
1224
+ .from(bylines)
1225
+ .where(isNull(bylines.deletedAt))
1226
+ .orderBy(desc(bylines.createdAt))
1227
+
1228
+ return {
1229
+ jsonrpc: "2.0",
1230
+ id,
1231
+ result: { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] }
1232
+ }
1233
+ }
1234
+
1235
+ case "cms_get_byline": {
1236
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
1237
+ args.slugOrId
1238
+ )
1239
+ const condition = isUuid ? eq(bylines.id, args.slugOrId) : eq(bylines.slug, args.slugOrId)
1240
+ const row = await db
1241
+ .select()
1242
+ .from(bylines)
1243
+ .where(and(isNull(bylines.deletedAt), condition))
1244
+ .limit(1)
1245
+
1246
+ return {
1247
+ jsonrpc: "2.0",
1248
+ id,
1249
+ result: { content: [{ type: "text", text: JSON.stringify(row[0] || null, null, 2) }] }
1250
+ }
1251
+ }
1252
+
1253
+ case "cms_create_byline": {
1254
+ const row = await db
1255
+ .insert(bylines)
1256
+ .values({
1257
+ name: args.name,
1258
+ slug: args.slug,
1259
+ websiteUrl: args.websiteUrl || null,
1260
+ bio: args.bio || null,
1261
+ avatar: args.avatar || null,
1262
+ userId: args.userId || null,
1263
+ socials: args.socials || {},
1264
+ metadata: args.metadata || {}
1265
+ })
1266
+ .returning()
1267
+
1268
+ return {
1269
+ jsonrpc: "2.0",
1270
+ id,
1271
+ result: { content: [{ type: "text", text: JSON.stringify(row[0], null, 2) }] }
1272
+ }
1273
+ }
1274
+
1275
+ case "cms_update_byline": {
1276
+ const updateData: Record<string, any> = { updatedAt: new Date() }
1277
+ if (args.name) updateData["name"] = args.name
1278
+ if (args.slug) updateData["slug"] = args.slug
1279
+ if (args.websiteUrl !== undefined) updateData["websiteUrl"] = args.websiteUrl
1280
+ if (args.bio !== undefined) updateData["bio"] = args.bio
1281
+ if (args.avatar !== undefined) updateData["avatar"] = args.avatar
1282
+ if (args.userId !== undefined) updateData["userId"] = args.userId
1283
+ if (args.socials) updateData["socials"] = args.socials
1284
+ if (args.metadata) updateData["metadata"] = args.metadata
1285
+
1286
+ const updated = await db
1287
+ .update(bylines)
1288
+ .set(updateData)
1289
+ .where(eq(bylines.id, args.id))
1290
+ .returning()
1291
+ return {
1292
+ jsonrpc: "2.0",
1293
+ id,
1294
+ result: { content: [{ type: "text", text: JSON.stringify(updated[0] || null, null, 2) }] }
1295
+ }
1296
+ }
1297
+
1298
+ case "cms_delete_byline": {
1299
+ const deleted = await db
1300
+ .update(bylines)
1301
+ .set({ deletedAt: new Date() })
1302
+ .where(eq(bylines.id, args.id))
1303
+ .returning()
1304
+ return {
1305
+ jsonrpc: "2.0",
1306
+ id,
1307
+ result: {
1308
+ content: [
1309
+ {
1310
+ type: "text",
1311
+ text: JSON.stringify({ success: true, byline: deleted[0] || null }, null, 2)
1312
+ }
1313
+ ]
1314
+ }
1315
+ }
1316
+ }
1317
+
1318
+ // ─── Dynamic Content Types & E-Commerce ───────────────────────────────────
1319
+ case "cms_list_content_types": {
1320
+ const rows = await db.select().from(contentTypes).orderBy(desc(contentTypes.createdAt))
1321
+ return {
1322
+ jsonrpc: "2.0",
1323
+ id,
1324
+ result: { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] }
1325
+ }
1326
+ }
1327
+
1328
+ case "cms_get_content_type": {
1329
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
1330
+ args.slugOrId
1331
+ )
1332
+ const condition = isUuid
1333
+ ? eq(contentTypes.id, args.slugOrId)
1334
+ : eq(contentTypes.slug, args.slugOrId)
1335
+ const row = await db.select().from(contentTypes).where(condition).limit(1)
1336
+
1337
+ return {
1338
+ jsonrpc: "2.0",
1339
+ id,
1340
+ result: { content: [{ type: "text", text: JSON.stringify(row[0] || null, null, 2) }] }
1341
+ }
1342
+ }
1343
+
1344
+ case "cms_create_content_type": {
1345
+ const namePayload = typeof args.name === "string" ? { en: args.name } : args.name
1346
+ const descPayload =
1347
+ typeof args.description === "string" ? { en: args.description } : args.description || null
1348
+
1349
+ const row = await db
1350
+ .insert(contentTypes)
1351
+ .values({
1352
+ slug: args.slug,
1353
+ name: namePayload,
1354
+ description: descPayload,
1355
+ icon: args.icon || "i-lucide-box",
1356
+ mode: args.mode || "document",
1357
+ fieldSchema: args.fieldSchema || {},
1358
+ ecomSettings: args.ecomSettings || null,
1359
+ initialBlocks: args.initialBlocks || [],
1360
+ isSystem: Boolean(args.isSystem)
1361
+ })
1362
+ .returning()
1363
+
1364
+ return {
1365
+ jsonrpc: "2.0",
1366
+ id,
1367
+ result: { content: [{ type: "text", text: JSON.stringify(row[0], null, 2) }] }
1368
+ }
1369
+ }
1370
+
1371
+ case "cms_update_content_type": {
1372
+ const updateData: Record<string, any> = { updatedAt: new Date() }
1373
+ if (args.slug) updateData["slug"] = args.slug
1374
+ if (args.name)
1375
+ updateData["name"] = typeof args.name === "string" ? { en: args.name } : args.name
1376
+ if (args.description !== undefined) {
1377
+ updateData["description"] =
1378
+ typeof args.description === "string" ? { en: args.description } : args.description
1379
+ }
1380
+ if (args.icon) updateData["icon"] = args.icon
1381
+ if (args.mode) updateData["mode"] = args.mode
1382
+ if (args.fieldSchema) updateData["fieldSchema"] = args.fieldSchema
1383
+ if (args.ecomSettings !== undefined) updateData["ecomSettings"] = args.ecomSettings
1384
+ if (args.initialBlocks) updateData["initialBlocks"] = args.initialBlocks
1385
+
1386
+ const updated = await db
1387
+ .update(contentTypes)
1388
+ .set(updateData)
1389
+ .where(eq(contentTypes.id, args.id))
1390
+ .returning()
1391
+ return {
1392
+ jsonrpc: "2.0",
1393
+ id,
1394
+ result: { content: [{ type: "text", text: JSON.stringify(updated[0] || null, null, 2) }] }
1395
+ }
1396
+ }
1397
+
1398
+ case "cms_delete_content_type": {
1399
+ const deleted = await db
1400
+ .delete(contentTypes)
1401
+ .where(eq(contentTypes.id, args.id))
1402
+ .returning()
1403
+ return {
1404
+ jsonrpc: "2.0",
1405
+ id,
1406
+ result: {
1407
+ content: [
1408
+ {
1409
+ type: "text",
1410
+ text: JSON.stringify({ success: true, contentType: deleted[0] || null }, null, 2)
1411
+ }
1412
+ ]
1413
+ }
1414
+ }
1415
+ }
1416
+
1417
+ // ─── Site Settings ────────────────────────────────────────────────────────
1418
+ case "cms_get_site_settings": {
1419
+ const settings = await getSiteSettings(db)
1420
+ return {
1421
+ jsonrpc: "2.0",
1422
+ id,
1423
+ result: { content: [{ type: "text", text: JSON.stringify(settings, null, 2) }] }
1424
+ }
1425
+ }
1426
+
1427
+ case "cms_update_site_settings": {
1428
+ const updated = await updateSiteSettings(db, args)
1429
+ return {
1430
+ jsonrpc: "2.0",
1431
+ id,
1432
+ result: { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] }
1433
+ }
1434
+ }
1435
+
1436
+ // ─── Search ───────────────────────────────────────────────────────────────
1437
+ case "cms_search_content": {
1438
+ const queryLimit = typeof args.limit === "number" ? args.limit : 10
1439
+ const conditions = [isNull(pages.deletedAt)]
1440
+ if (args.type) {
1441
+ conditions.push(eq(pages.type, args.type as PageType))
1442
+ }
1443
+
1444
+ const rows = await db
1445
+ .select({
1446
+ id: pages.id,
1447
+ slug: pages.slug,
1448
+ type: pages.type,
1449
+ title: pages.title,
1450
+ description: pages.description,
1451
+ postedAt: pages.postedAt
1452
+ })
1453
+ .from(pages)
1454
+ .where(
1455
+ and(
1456
+ ...conditions,
1457
+ or(
1458
+ like(pages.slug, `%${args.query}%`),
1459
+ sql`CAST(${pages.title} AS TEXT) ILIKE ${`%${args.query}%`}`
1460
+ )
1461
+ )
1462
+ )
1463
+ .limit(queryLimit)
1464
+
1465
+ return {
1466
+ jsonrpc: "2.0",
1467
+ id,
1468
+ result: { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] }
1469
+ }
1470
+ }
1471
+
1472
+ default:
1473
+ return {
1474
+ jsonrpc: "2.0",
1475
+ id,
1476
+ error: { code: -32601, message: `Method or tool '${name}' not found` }
1477
+ }
217
1478
  }
218
1479
  }
219
1480
 
220
1481
  return {
221
1482
  jsonrpc: "2.0",
222
1483
  id,
223
- error: {
224
- code: -32601,
225
- message: `Unknown method: ${method}`
226
- }
1484
+ error: { code: -32601, message: `Unknown method: ${method}` }
227
1485
  }
228
1486
  }