@cmssy/mcp-server 0.5.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/queries.d.ts +24 -9
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +223 -9
- package/dist/queries.js.map +1 -1
- package/dist/responses.d.ts +71 -0
- package/dist/responses.d.ts.map +1 -0
- package/dist/responses.js +87 -0
- package/dist/responses.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +565 -144
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { mediaTypeValues } from "@cmssy/types";
|
|
6
7
|
// Read our own version from package.json so the MCP handshake
|
|
7
8
|
// advertises what the user actually installed, instead of drifting
|
|
8
9
|
// whenever we bump the package.
|
|
@@ -33,7 +34,8 @@ const jsonPreprocess = (val) => {
|
|
|
33
34
|
return val;
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
|
-
import { PAGES_QUERY, PAGE_BY_ID_QUERY, WORKSPACE_BLOCKS_QUERY, WORKSPACE_BLOCK_BY_TYPE_QUERY, SITE_CONFIG_QUERY, CURRENT_WORKSPACE_QUERY, MEDIA_ASSETS_QUERY, SAVE_PAGE_MUTATION, PATCH_BLOCK_CONTENT_MUTATION, UPDATE_PAGE_SETTINGS_MUTATION, TOGGLE_PUBLISH_MUTATION, PUBLISH_PAGE_MUTATION, REVERT_TO_PUBLISHED_MUTATION, REMOVE_PAGE_MUTATION, UPDATE_PAGE_LAYOUT_MUTATION, FORMS_QUERY, FORM_BY_ID_QUERY, FORM_SUBMISSIONS_QUERY, FORM_SUBMISSION_BY_ID_QUERY, CREATE_FORM_MUTATION, UPDATE_FORM_MUTATION, DELETE_FORM_MUTATION, UPDATE_FORM_SUBMISSION_STATUS_MUTATION, DELETE_FORM_SUBMISSION_MUTATION, } from "./queries.js";
|
|
37
|
+
import { PAGES_QUERY, PAGE_BY_ID_QUERY, WORKSPACE_BLOCKS_QUERY, WORKSPACE_BLOCK_BY_TYPE_QUERY, SITE_CONFIG_QUERY, CURRENT_WORKSPACE_QUERY, MEDIA_ASSETS_QUERY, SAVE_PAGE_MUTATION, PATCH_BLOCK_CONTENT_MUTATION, UPDATE_PAGE_SETTINGS_MUTATION, TOGGLE_PUBLISH_MUTATION, PUBLISH_PAGE_MUTATION, REVERT_TO_PUBLISHED_MUTATION, REMOVE_PAGE_MUTATION, UPDATE_PAGE_LAYOUT_MUTATION, FORMS_QUERY, FORM_BY_ID_QUERY, FORM_SUBMISSIONS_QUERY, FORM_SUBMISSION_BY_ID_QUERY, CREATE_FORM_MUTATION, UPDATE_FORM_MUTATION, DELETE_FORM_MUTATION, UPDATE_FORM_SUBMISSION_STATUS_MUTATION, DELETE_FORM_SUBMISSION_MUTATION, MODEL_DEFINITIONS_QUERY, MODEL_DEFINITIONS_BY_SLUG_INDEX_QUERY, MODEL_DEFINITION_BY_ID_QUERY, MODEL_RECORDS_QUERY, MODEL_RECORD_BY_ID_QUERY, MODEL_TEMPLATES_QUERY, CREATE_MODEL_DEFINITION_MUTATION, UPDATE_MODEL_DEFINITION_MUTATION, DELETE_MODEL_DEFINITION_MUTATION, CREATE_MODEL_RECORD_MUTATION, UPDATE_MODEL_RECORD_MUTATION, UPDATE_MODEL_RECORD_STATUS_MUTATION, DELETE_MODEL_RECORD_MUTATION, IMPORT_MODEL_RECORDS_MUTATION, INSTALL_MODEL_TEMPLATE_MUTATION, } from "./queries.js";
|
|
38
|
+
import { responseModeSchema, pageMinimal, pageBlockMinimal, formMinimal, modelMinimal, recordMinimal, jsonText, } from "./responses.js";
|
|
37
39
|
export function createServer(client) {
|
|
38
40
|
const server = new McpServer({
|
|
39
41
|
name: "cmssy",
|
|
@@ -232,7 +234,7 @@ export function createServer(client) {
|
|
|
232
234
|
};
|
|
233
235
|
});
|
|
234
236
|
// ─── Write Tools ─────────────────────────────────────────────
|
|
235
|
-
server.tool("create_page", "Create a new page. Returns
|
|
237
|
+
server.tool("create_page", "Create a new page. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
236
238
|
name: z.string().describe("Internal page name"),
|
|
237
239
|
slug: z.string().describe("URL slug (e.g. 'about', 'features')"),
|
|
238
240
|
parentId: z
|
|
@@ -256,7 +258,8 @@ export function createServer(client) {
|
|
|
256
258
|
.preprocess(jsonPreprocess, z.record(z.string(), z.string()))
|
|
257
259
|
.optional()
|
|
258
260
|
.describe("Multilingual SEO description"),
|
|
259
|
-
|
|
261
|
+
response: responseModeSchema,
|
|
262
|
+
}, async ({ name, slug, parentId, pageType, displayName, seoTitle, seoDescription, response, }) => {
|
|
260
263
|
const input = { name, slug };
|
|
261
264
|
if (parentId)
|
|
262
265
|
input.parentId = parentId;
|
|
@@ -271,16 +274,9 @@ export function createServer(client) {
|
|
|
271
274
|
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
272
275
|
input,
|
|
273
276
|
});
|
|
274
|
-
return
|
|
275
|
-
content: [
|
|
276
|
-
{
|
|
277
|
-
type: "text",
|
|
278
|
-
text: JSON.stringify(data.savePage, null, 2),
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
};
|
|
277
|
+
return jsonText(response, data.savePage, pageMinimal);
|
|
282
278
|
});
|
|
283
|
-
server.tool("update_page_blocks", "Set the full content blocks array on a page. Replaces all existing content blocks. Block types are validated against workspace config. Blocks with matching IDs preserve their existing content/settings when not explicitly provided.", {
|
|
279
|
+
server.tool("update_page_blocks", "Set the full content blocks array on a page. Replaces all existing content blocks. Block types are validated against workspace config. Blocks with matching IDs preserve their existing content/settings when not explicitly provided. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
284
280
|
pageId: z.string().describe("Page ID"),
|
|
285
281
|
blocks: z.preprocess(jsonPreprocess, z
|
|
286
282
|
.array(z.object({
|
|
@@ -300,7 +296,8 @@ export function createServer(client) {
|
|
|
300
296
|
blockVersion: z.string().optional(),
|
|
301
297
|
}))
|
|
302
298
|
.describe("Full array of content blocks to set on the page")),
|
|
303
|
-
|
|
299
|
+
response: responseModeSchema,
|
|
300
|
+
}, async ({ pageId, blocks, response }) => {
|
|
304
301
|
// Validate all block types against workspace registry
|
|
305
302
|
const validation = await validateBlockTypes(blocks.map((b) => b.type));
|
|
306
303
|
if (!validation.valid) {
|
|
@@ -347,16 +344,9 @@ export function createServer(client) {
|
|
|
347
344
|
blocks: mergedBlocks,
|
|
348
345
|
},
|
|
349
346
|
});
|
|
350
|
-
return
|
|
351
|
-
content: [
|
|
352
|
-
{
|
|
353
|
-
type: "text",
|
|
354
|
-
text: JSON.stringify(data.savePage, null, 2),
|
|
355
|
-
},
|
|
356
|
-
],
|
|
357
|
-
};
|
|
347
|
+
return jsonText(response, data.savePage, pageMinimal);
|
|
358
348
|
});
|
|
359
|
-
server.tool("update_page_settings", "Update page metadata: name, slug, display name, SEO fields", {
|
|
349
|
+
server.tool("update_page_settings", "Update page metadata: name, slug, display name, SEO fields. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
360
350
|
id: z.string().describe("Page ID"),
|
|
361
351
|
name: z.string().optional().describe("Internal page name"),
|
|
362
352
|
slug: z.string().optional().describe("URL slug"),
|
|
@@ -373,7 +363,8 @@ export function createServer(client) {
|
|
|
373
363
|
.optional()
|
|
374
364
|
.describe("Multilingual SEO description"),
|
|
375
365
|
seoKeywords: z.array(z.string()).optional().describe("SEO keywords"),
|
|
376
|
-
|
|
366
|
+
response: responseModeSchema,
|
|
367
|
+
}, async ({ id, name, slug, displayName, seoTitle, seoDescription, seoKeywords, response, }) => {
|
|
377
368
|
const input = { id };
|
|
378
369
|
if (name !== undefined)
|
|
379
370
|
input.name = name;
|
|
@@ -388,16 +379,12 @@ export function createServer(client) {
|
|
|
388
379
|
if (seoKeywords !== undefined)
|
|
389
380
|
input.seoKeywords = seoKeywords;
|
|
390
381
|
const data = await client.query(UPDATE_PAGE_SETTINGS_MUTATION, { input });
|
|
391
|
-
return
|
|
392
|
-
content: [
|
|
393
|
-
{
|
|
394
|
-
type: "text",
|
|
395
|
-
text: JSON.stringify(data.updatePageSettings, null, 2),
|
|
396
|
-
},
|
|
397
|
-
],
|
|
398
|
-
};
|
|
382
|
+
return jsonText(response, data.updatePageSettings, pageMinimal);
|
|
399
383
|
});
|
|
400
|
-
server.tool("publish_page", "Publish a page or re-publish with latest draft changes. Uses atomic publishPage mutation.
|
|
384
|
+
server.tool("publish_page", "Publish a page or re-publish with latest draft changes. Uses atomic publishPage mutation. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
385
|
+
pageId: z.string().describe("Page ID to publish"),
|
|
386
|
+
response: responseModeSchema,
|
|
387
|
+
}, async ({ pageId, response }) => {
|
|
401
388
|
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
402
389
|
if (!pageData.page) {
|
|
403
390
|
return {
|
|
@@ -406,7 +393,9 @@ export function createServer(client) {
|
|
|
406
393
|
};
|
|
407
394
|
}
|
|
408
395
|
const page = pageData.page;
|
|
409
|
-
if (page.published &&
|
|
396
|
+
if (page.published &&
|
|
397
|
+
!page.hasUnpublishedContentChanges &&
|
|
398
|
+
!page.hasUnpublishedLayoutChanges) {
|
|
410
399
|
return {
|
|
411
400
|
content: [
|
|
412
401
|
{
|
|
@@ -429,16 +418,12 @@ export function createServer(client) {
|
|
|
429
418
|
blockVersion: b.blockVersion,
|
|
430
419
|
}));
|
|
431
420
|
const data = await client.query(PUBLISH_PAGE_MUTATION, { id: pageId, blocks });
|
|
432
|
-
return {
|
|
433
|
-
content: [
|
|
434
|
-
{
|
|
435
|
-
type: "text",
|
|
436
|
-
text: JSON.stringify(data.publishPage, null, 2),
|
|
437
|
-
},
|
|
438
|
-
],
|
|
439
|
-
};
|
|
421
|
+
return jsonText(response, data.publishPage, (p) => pageMinimal(p, { published: true }));
|
|
440
422
|
});
|
|
441
|
-
server.tool("unpublish_page", "Unpublish a published page (toggles published state off).
|
|
423
|
+
server.tool("unpublish_page", "Unpublish a published page (toggles published state off). Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
424
|
+
pageId: z.string().describe("Page ID to unpublish"),
|
|
425
|
+
response: responseModeSchema,
|
|
426
|
+
}, async ({ pageId, response }) => {
|
|
442
427
|
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
443
428
|
if (!pageData.page) {
|
|
444
429
|
return {
|
|
@@ -454,16 +439,12 @@ export function createServer(client) {
|
|
|
454
439
|
};
|
|
455
440
|
}
|
|
456
441
|
const data = await client.query(TOGGLE_PUBLISH_MUTATION, { id: pageId });
|
|
457
|
-
return {
|
|
458
|
-
content: [
|
|
459
|
-
{
|
|
460
|
-
type: "text",
|
|
461
|
-
text: JSON.stringify(data.togglePublish, null, 2),
|
|
462
|
-
},
|
|
463
|
-
],
|
|
464
|
-
};
|
|
442
|
+
return jsonText(response, data.togglePublish, (p) => pageMinimal(p, { published: false }));
|
|
465
443
|
});
|
|
466
|
-
server.tool("revert_to_published", "Discard all draft changes and revert a page to its last published version.
|
|
444
|
+
server.tool("revert_to_published", "Discard all draft changes and revert a page to its last published version. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
445
|
+
pageId: z.string().describe("Page ID to revert"),
|
|
446
|
+
response: responseModeSchema,
|
|
447
|
+
}, async ({ pageId, response }) => {
|
|
467
448
|
const data = await client.query(REVERT_TO_PUBLISHED_MUTATION, { id: pageId });
|
|
468
449
|
if (!data.revertToPublished) {
|
|
469
450
|
return {
|
|
@@ -476,14 +457,7 @@ export function createServer(client) {
|
|
|
476
457
|
isError: true,
|
|
477
458
|
};
|
|
478
459
|
}
|
|
479
|
-
return
|
|
480
|
-
content: [
|
|
481
|
-
{
|
|
482
|
-
type: "text",
|
|
483
|
-
text: JSON.stringify(data.revertToPublished, null, 2),
|
|
484
|
-
},
|
|
485
|
-
],
|
|
486
|
-
};
|
|
460
|
+
return jsonText(response, data.revertToPublished, pageMinimal);
|
|
487
461
|
});
|
|
488
462
|
server.tool("delete_page", "Permanently delete a page and all its descendants. Cannot delete the homepage.", { pageId: z.string().describe("Page ID to delete") }, async ({ pageId }) => {
|
|
489
463
|
const data = await client.query(REMOVE_PAGE_MUTATION, { id: pageId });
|
|
@@ -499,7 +473,7 @@ export function createServer(client) {
|
|
|
499
473
|
};
|
|
500
474
|
});
|
|
501
475
|
// ─── Layout Tools ──────────────────────────────────────────
|
|
502
|
-
server.tool("update_page_layout", "Update page-level layout settings: inheritance, overrides, or replace all layout blocks. Block types are validated against workspace config.", {
|
|
476
|
+
server.tool("update_page_layout", "Update page-level layout settings: inheritance, overrides, or replace all layout blocks. Block types are validated against workspace config. Returns a minimal ack by default; pass response='full' for the full mutation response.", {
|
|
503
477
|
pageId: z.string().describe("Page ID"),
|
|
504
478
|
layoutBlocks: z
|
|
505
479
|
.array(z.record(z.string(), z.unknown()))
|
|
@@ -517,7 +491,8 @@ export function createServer(client) {
|
|
|
517
491
|
.boolean()
|
|
518
492
|
.optional()
|
|
519
493
|
.describe("Whether this page inherits layout from parent"),
|
|
520
|
-
|
|
494
|
+
response: responseModeSchema,
|
|
495
|
+
}, async ({ pageId, layoutBlocks, layoutOverrides, inheritsLayout, response, }) => {
|
|
521
496
|
// Validate block types against workspace registry
|
|
522
497
|
if (layoutBlocks) {
|
|
523
498
|
const types = layoutBlocks
|
|
@@ -568,17 +543,10 @@ export function createServer(client) {
|
|
|
568
543
|
isError: true,
|
|
569
544
|
};
|
|
570
545
|
}
|
|
571
|
-
return
|
|
572
|
-
content: [
|
|
573
|
-
{
|
|
574
|
-
type: "text",
|
|
575
|
-
text: JSON.stringify(data.updatePageLayout, null, 2),
|
|
576
|
-
},
|
|
577
|
-
],
|
|
578
|
-
};
|
|
546
|
+
return jsonText(response, data.updatePageLayout, pageMinimal);
|
|
579
547
|
});
|
|
580
548
|
// ─── Block Helper Tools (read-modify-write) ─────────────────
|
|
581
|
-
server.tool("add_block_to_page", "Add a block to a page. Automatically detects layout vs content block from workspace config. Auto-generates UUID and translation status.", {
|
|
549
|
+
server.tool("add_block_to_page", "Add a block to a page. Automatically detects layout vs content block from workspace config. Auto-generates UUID and translation status. Returns a minimal ack by default ({pageId, blockId, hasUnpublishedChanges, updatedAt}); pass response='full' for the full mutation response.", {
|
|
582
550
|
pageId: z.string().describe("Page ID"),
|
|
583
551
|
block: z.preprocess(jsonPreprocess, z.object({
|
|
584
552
|
type: z
|
|
@@ -594,7 +562,8 @@ export function createServer(client) {
|
|
|
594
562
|
.number()
|
|
595
563
|
.optional()
|
|
596
564
|
.describe("0-based position to insert at (default: end)"),
|
|
597
|
-
|
|
565
|
+
response: responseModeSchema,
|
|
566
|
+
}, async ({ pageId, block, position, response }) => {
|
|
598
567
|
// Validate block type against workspace registry
|
|
599
568
|
const blockDef = await findBlockDef(block.type);
|
|
600
569
|
if (!blockDef) {
|
|
@@ -654,14 +623,18 @@ export function createServer(client) {
|
|
|
654
623
|
};
|
|
655
624
|
const layoutBlocks = [...existingLayoutBlocks, newLayoutBlock];
|
|
656
625
|
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks } });
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
626
|
+
if (!data.updatePageLayout) {
|
|
627
|
+
return {
|
|
628
|
+
content: [
|
|
629
|
+
{
|
|
630
|
+
type: "text",
|
|
631
|
+
text: `Failed to update layout while adding block to page '${pageId}'`,
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
isError: true,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
return jsonText(response, { blockId: newBlockId, page: data.updatePageLayout }, () => pageBlockMinimal(data.updatePageLayout, newBlockId));
|
|
665
638
|
}
|
|
666
639
|
else {
|
|
667
640
|
// Content block — add to blocks via savePage
|
|
@@ -692,24 +665,18 @@ export function createServer(client) {
|
|
|
692
665
|
blocks,
|
|
693
666
|
},
|
|
694
667
|
});
|
|
695
|
-
return {
|
|
696
|
-
content: [
|
|
697
|
-
{
|
|
698
|
-
type: "text",
|
|
699
|
-
text: JSON.stringify({ blockId: newBlockId, page: data.savePage }, null, 2),
|
|
700
|
-
},
|
|
701
|
-
],
|
|
702
|
-
};
|
|
668
|
+
return jsonText(response, { blockId: newBlockId, page: data.savePage }, () => pageBlockMinimal(data.savePage, newBlockId));
|
|
703
669
|
}
|
|
704
670
|
});
|
|
705
|
-
server.tool("update_block_content", "Update a specific block's content on a page. Merges with existing content. Works for both content and layout blocks.", {
|
|
671
|
+
server.tool("update_block_content", "Update a specific block's content on a page. Merges with existing content. Works for both content and layout blocks. Returns a minimal ack by default ({pageId, blockId, hasUnpublishedChanges, updatedAt}); pass response='full' for the full mutation response.", {
|
|
706
672
|
pageId: z.string().describe("Page ID"),
|
|
707
673
|
blockId: z.string().describe("Block instance ID (UUID) to update"),
|
|
708
674
|
content: z
|
|
709
675
|
.record(z.string(), z.unknown())
|
|
710
676
|
.describe("Content to merge: { en: { title: 'New Title' } }"),
|
|
711
677
|
settings: z.record(z.string(), z.unknown()).optional(),
|
|
712
|
-
|
|
678
|
+
response: responseModeSchema,
|
|
679
|
+
}, async ({ pageId, blockId, content, settings, response }) => {
|
|
713
680
|
// Fetch current page
|
|
714
681
|
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
715
682
|
if (!pageData.page) {
|
|
@@ -775,14 +742,18 @@ export function createServer(client) {
|
|
|
775
742
|
targetArray[targetIndex] = existingBlock;
|
|
776
743
|
if (isLayout) {
|
|
777
744
|
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks: targetArray } });
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
745
|
+
if (!data.updatePageLayout) {
|
|
746
|
+
return {
|
|
747
|
+
content: [
|
|
748
|
+
{
|
|
749
|
+
type: "text",
|
|
750
|
+
text: `Failed to update layout while editing block '${blockId}' on page '${pageId}'`,
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
isError: true,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
return jsonText(response, data.updatePageLayout, (p) => pageBlockMinimal(p, blockId));
|
|
786
757
|
}
|
|
787
758
|
else {
|
|
788
759
|
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
@@ -794,14 +765,7 @@ export function createServer(client) {
|
|
|
794
765
|
blocks: targetArray,
|
|
795
766
|
},
|
|
796
767
|
});
|
|
797
|
-
return
|
|
798
|
-
content: [
|
|
799
|
-
{
|
|
800
|
-
type: "text",
|
|
801
|
-
text: JSON.stringify(data.savePage, null, 2),
|
|
802
|
-
},
|
|
803
|
-
],
|
|
804
|
-
};
|
|
768
|
+
return jsonText(response, data.savePage, (p) => pageBlockMinimal(p, blockId));
|
|
805
769
|
}
|
|
806
770
|
});
|
|
807
771
|
server.tool("patch_block_content", "Apply surgical HTML edits (insert_before/insert_after/replace_section) to a block's localized content string (e.g. a docs-article HTML body) without re-sending the full content. Use this for small edits where you have a unique anchor substring in the existing HTML - it's ~10x cheaper in tokens than update_block_content and rejects any op whose marker is missing or ambiguous. For replace_section: startMarker is inclusive, endMarker is exclusive.", {
|
|
@@ -898,19 +862,29 @@ export function createServer(client) {
|
|
|
898
862
|
isError: true,
|
|
899
863
|
};
|
|
900
864
|
}
|
|
865
|
+
// Compact JSON: patch_block_content is ack-only (no pretty-print
|
|
866
|
+
// needed), matching the compact shape used by minimal-mode returns.
|
|
867
|
+
const patched = data.patchBlockContent;
|
|
901
868
|
return {
|
|
902
869
|
content: [
|
|
903
870
|
{
|
|
904
871
|
type: "text",
|
|
905
|
-
text: JSON.stringify(
|
|
872
|
+
text: JSON.stringify({
|
|
873
|
+
id: patched.id,
|
|
874
|
+
slug: patched.slug,
|
|
875
|
+
hasUnpublishedChanges: Boolean(patched.hasUnpublishedContentChanges) ||
|
|
876
|
+
Boolean(patched.hasUnpublishedLayoutChanges),
|
|
877
|
+
updatedAt: patched.updatedAt,
|
|
878
|
+
}),
|
|
906
879
|
},
|
|
907
880
|
],
|
|
908
881
|
};
|
|
909
882
|
});
|
|
910
|
-
server.tool("remove_block_from_page", "Remove a specific block from a page by its instance ID. Works for both content and layout blocks.", {
|
|
883
|
+
server.tool("remove_block_from_page", "Remove a specific block from a page by its instance ID. Works for both content and layout blocks. Returns a minimal ack by default ({pageId, blockId, hasUnpublishedChanges, updatedAt}); pass response='full' for the full mutation response.", {
|
|
911
884
|
pageId: z.string().describe("Page ID"),
|
|
912
885
|
blockId: z.string().describe("Block instance ID (UUID) to remove"),
|
|
913
|
-
|
|
886
|
+
response: responseModeSchema,
|
|
887
|
+
}, async ({ pageId, blockId, response }) => {
|
|
914
888
|
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
915
889
|
if (!pageData.page) {
|
|
916
890
|
return {
|
|
@@ -931,27 +905,24 @@ export function createServer(client) {
|
|
|
931
905
|
blocks: contentBlocks,
|
|
932
906
|
},
|
|
933
907
|
});
|
|
934
|
-
return
|
|
935
|
-
content: [
|
|
936
|
-
{
|
|
937
|
-
type: "text",
|
|
938
|
-
text: JSON.stringify(data.savePage, null, 2),
|
|
939
|
-
},
|
|
940
|
-
],
|
|
941
|
-
};
|
|
908
|
+
return jsonText(response, data.savePage, (p) => pageBlockMinimal(p, blockId));
|
|
942
909
|
}
|
|
943
910
|
// Try layout blocks
|
|
944
911
|
const layoutBlocks = (page.layoutBlocks || []).filter((b) => b.id !== blockId);
|
|
945
912
|
if (layoutBlocks.length < (page.layoutBlocks || []).length) {
|
|
946
913
|
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks } });
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
914
|
+
if (!data.updatePageLayout) {
|
|
915
|
+
return {
|
|
916
|
+
content: [
|
|
917
|
+
{
|
|
918
|
+
type: "text",
|
|
919
|
+
text: `Failed to update layout while removing block '${blockId}' from page '${pageId}'`,
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
isError: true,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return jsonText(response, data.updatePageLayout, (p) => pageBlockMinimal(p, blockId));
|
|
955
926
|
}
|
|
956
927
|
return {
|
|
957
928
|
content: [
|
|
@@ -1007,7 +978,7 @@ export function createServer(client) {
|
|
|
1007
978
|
],
|
|
1008
979
|
};
|
|
1009
980
|
});
|
|
1010
|
-
server.tool("create_form", "Create a new form with fields and settings. Returns the
|
|
981
|
+
server.tool("create_form", "Create a new form with fields and settings. Returns a minimal ack by default; pass response='full' for the full form.", {
|
|
1011
982
|
name: z.string().describe("Form name"),
|
|
1012
983
|
slug: z.string().describe("URL-friendly slug (must be unique)"),
|
|
1013
984
|
description: z.string().optional().describe("Form description"),
|
|
@@ -1090,7 +1061,8 @@ export function createServer(client) {
|
|
|
1090
1061
|
}))
|
|
1091
1062
|
.optional()
|
|
1092
1063
|
.describe("Form settings (action type, notifications, etc.)"),
|
|
1093
|
-
|
|
1064
|
+
response: responseModeSchema,
|
|
1065
|
+
}, async ({ name, slug, description, fields, settings, response }) => {
|
|
1094
1066
|
const input = { name, slug };
|
|
1095
1067
|
if (description)
|
|
1096
1068
|
input.description = description;
|
|
@@ -1099,16 +1071,9 @@ export function createServer(client) {
|
|
|
1099
1071
|
if (settings)
|
|
1100
1072
|
input.settings = settings;
|
|
1101
1073
|
const data = await client.query(CREATE_FORM_MUTATION, { input });
|
|
1102
|
-
return
|
|
1103
|
-
content: [
|
|
1104
|
-
{
|
|
1105
|
-
type: "text",
|
|
1106
|
-
text: JSON.stringify(data.createForm, null, 2),
|
|
1107
|
-
},
|
|
1108
|
-
],
|
|
1109
|
-
};
|
|
1074
|
+
return jsonText(response, data.createForm, formMinimal);
|
|
1110
1075
|
});
|
|
1111
|
-
server.tool("update_form", "Update an existing form's name, slug, status, fields, or settings.", {
|
|
1076
|
+
server.tool("update_form", "Update an existing form's name, slug, status, fields, or settings. Returns a minimal ack by default; pass response='full' for the full form.", {
|
|
1112
1077
|
formId: z.string().describe("Form ID to update"),
|
|
1113
1078
|
name: z.string().optional().describe("New form name"),
|
|
1114
1079
|
slug: z.string().optional().describe("New slug"),
|
|
@@ -1188,7 +1153,8 @@ export function createServer(client) {
|
|
|
1188
1153
|
}))
|
|
1189
1154
|
.optional()
|
|
1190
1155
|
.describe("Updated settings"),
|
|
1191
|
-
|
|
1156
|
+
response: responseModeSchema,
|
|
1157
|
+
}, async ({ formId, name, slug, description, status, fields, settings, response, }) => {
|
|
1192
1158
|
const input = {};
|
|
1193
1159
|
if (name !== undefined)
|
|
1194
1160
|
input.name = name;
|
|
@@ -1211,14 +1177,7 @@ export function createServer(client) {
|
|
|
1211
1177
|
isError: true,
|
|
1212
1178
|
};
|
|
1213
1179
|
}
|
|
1214
|
-
return
|
|
1215
|
-
content: [
|
|
1216
|
-
{
|
|
1217
|
-
type: "text",
|
|
1218
|
-
text: JSON.stringify(data.updateForm, null, 2),
|
|
1219
|
-
},
|
|
1220
|
-
],
|
|
1221
|
-
};
|
|
1180
|
+
return jsonText(response, data.updateForm, formMinimal);
|
|
1222
1181
|
});
|
|
1223
1182
|
server.tool("delete_form", "Permanently delete a form and all its submissions.", {
|
|
1224
1183
|
formId: z.string().describe("Form ID to delete"),
|
|
@@ -1311,6 +1270,468 @@ export function createServer(client) {
|
|
|
1311
1270
|
],
|
|
1312
1271
|
};
|
|
1313
1272
|
});
|
|
1273
|
+
// ─── Model Tools (Custom Data Models) ────────────────────────
|
|
1274
|
+
const propertyFieldTypes = [
|
|
1275
|
+
"string",
|
|
1276
|
+
"richtext",
|
|
1277
|
+
"number",
|
|
1278
|
+
"boolean",
|
|
1279
|
+
"date",
|
|
1280
|
+
"datetime",
|
|
1281
|
+
"email",
|
|
1282
|
+
"url",
|
|
1283
|
+
"media",
|
|
1284
|
+
"relation",
|
|
1285
|
+
"select",
|
|
1286
|
+
"multiselect",
|
|
1287
|
+
"object",
|
|
1288
|
+
"list",
|
|
1289
|
+
];
|
|
1290
|
+
const relationTypes = ["hasOne", "hasMany", "manyToMany"];
|
|
1291
|
+
const propertyFieldSchema = z.lazy(() => z.object({
|
|
1292
|
+
key: z.string().describe("Field key (identifier in record data)"),
|
|
1293
|
+
label: z.string().describe("Human-readable field label"),
|
|
1294
|
+
type: z
|
|
1295
|
+
.enum(propertyFieldTypes)
|
|
1296
|
+
.describe("Field type — controls validation + UI"),
|
|
1297
|
+
required: z.boolean().optional(),
|
|
1298
|
+
description: z.string().optional(),
|
|
1299
|
+
defaultValue: z.string().optional(),
|
|
1300
|
+
options: z
|
|
1301
|
+
.array(z.string())
|
|
1302
|
+
.optional()
|
|
1303
|
+
.describe("For select/multiselect: allowed values"),
|
|
1304
|
+
fields: z
|
|
1305
|
+
.array(propertyFieldSchema)
|
|
1306
|
+
.optional()
|
|
1307
|
+
.describe("For type=object: nested fields"),
|
|
1308
|
+
itemType: z
|
|
1309
|
+
.enum(propertyFieldTypes)
|
|
1310
|
+
.optional()
|
|
1311
|
+
.describe("For type=list: item type"),
|
|
1312
|
+
itemFields: z
|
|
1313
|
+
.array(propertyFieldSchema)
|
|
1314
|
+
.optional()
|
|
1315
|
+
.describe("For type=list of objects: nested field defs"),
|
|
1316
|
+
relationTo: z
|
|
1317
|
+
.string()
|
|
1318
|
+
.optional()
|
|
1319
|
+
.describe("For type=relation: target model slug"),
|
|
1320
|
+
relationType: z.enum(relationTypes).optional(),
|
|
1321
|
+
acceptedTypes: z
|
|
1322
|
+
.array(z.enum(mediaTypeValues))
|
|
1323
|
+
.optional()
|
|
1324
|
+
.describe("For type=media: allowed media kinds (empty = all)"),
|
|
1325
|
+
multiple: z
|
|
1326
|
+
.boolean()
|
|
1327
|
+
.optional()
|
|
1328
|
+
.describe("For type=media: true = gallery, false = single"),
|
|
1329
|
+
schemaProperty: z.string().optional(),
|
|
1330
|
+
minLength: z.number().int().optional(),
|
|
1331
|
+
maxLength: z.number().int().optional(),
|
|
1332
|
+
minValue: z.number().optional(),
|
|
1333
|
+
maxValue: z.number().optional(),
|
|
1334
|
+
pattern: z.string().optional(),
|
|
1335
|
+
}));
|
|
1336
|
+
const statusFieldSchema = z.object({
|
|
1337
|
+
enabled: z.boolean().optional(),
|
|
1338
|
+
values: z
|
|
1339
|
+
.array(z.string())
|
|
1340
|
+
.optional()
|
|
1341
|
+
.describe("Allowed status values (e.g. ['draft','active'])"),
|
|
1342
|
+
defaultValue: z.string().optional(),
|
|
1343
|
+
transitions: z
|
|
1344
|
+
.array(z.object({
|
|
1345
|
+
from: z.string(),
|
|
1346
|
+
to: z.array(z.string()),
|
|
1347
|
+
}))
|
|
1348
|
+
.optional()
|
|
1349
|
+
.describe("Allowed transitions: from one state to a set of states"),
|
|
1350
|
+
});
|
|
1351
|
+
const defaultSortSchema = z.object({
|
|
1352
|
+
field: z.string(),
|
|
1353
|
+
direction: z.enum(["asc", "desc"]),
|
|
1354
|
+
});
|
|
1355
|
+
const OBJECT_ID_RE = /^[a-f0-9]{24}$/i;
|
|
1356
|
+
// Match backend: apps/backend/src/models/model-definition.ts
|
|
1357
|
+
const SLUG_RE = /^[a-z][a-z0-9-]*$/;
|
|
1358
|
+
const SLUG_MSG = "Slug must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens";
|
|
1359
|
+
server.tool("list_models", "List all Custom Data Models (ModelDefinition) in the workspace.", {}, async () => {
|
|
1360
|
+
const data = await client.query(MODEL_DEFINITIONS_QUERY);
|
|
1361
|
+
return {
|
|
1362
|
+
content: [
|
|
1363
|
+
{
|
|
1364
|
+
type: "text",
|
|
1365
|
+
text: JSON.stringify(data.modelDefinitions, null, 2),
|
|
1366
|
+
},
|
|
1367
|
+
],
|
|
1368
|
+
};
|
|
1369
|
+
});
|
|
1370
|
+
server.tool("get_model", "Get a ModelDefinition by id (24-hex ObjectId) or by slug. Returns full definition including fields, statusField, and defaultSort.", {
|
|
1371
|
+
idOrSlug: z.string().describe("Model id (ObjectId) or slug"),
|
|
1372
|
+
}, async ({ idOrSlug }) => {
|
|
1373
|
+
if (OBJECT_ID_RE.test(idOrSlug)) {
|
|
1374
|
+
const data = await client.query(MODEL_DEFINITION_BY_ID_QUERY, { id: idOrSlug });
|
|
1375
|
+
if (data.modelDefinition) {
|
|
1376
|
+
return {
|
|
1377
|
+
content: [
|
|
1378
|
+
{
|
|
1379
|
+
type: "text",
|
|
1380
|
+
text: JSON.stringify(data.modelDefinition, null, 2),
|
|
1381
|
+
},
|
|
1382
|
+
],
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
// Fallback: resolve slug → id via a lightweight list (id + slug only),
|
|
1387
|
+
// then fetch the full definition for that single model. Avoids
|
|
1388
|
+
// pulling every model's fields when only one is needed.
|
|
1389
|
+
const index = await client.query(MODEL_DEFINITIONS_BY_SLUG_INDEX_QUERY);
|
|
1390
|
+
const match = index.modelDefinitions.find((m) => m.slug === idOrSlug);
|
|
1391
|
+
if (!match) {
|
|
1392
|
+
return {
|
|
1393
|
+
content: [
|
|
1394
|
+
{ type: "text", text: `Model '${idOrSlug}' not found` },
|
|
1395
|
+
],
|
|
1396
|
+
isError: true,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
const full = await client.query(MODEL_DEFINITION_BY_ID_QUERY, { id: match.id });
|
|
1400
|
+
if (!full.modelDefinition) {
|
|
1401
|
+
return {
|
|
1402
|
+
content: [
|
|
1403
|
+
{ type: "text", text: `Model '${idOrSlug}' not found` },
|
|
1404
|
+
],
|
|
1405
|
+
isError: true,
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
return {
|
|
1409
|
+
content: [
|
|
1410
|
+
{
|
|
1411
|
+
type: "text",
|
|
1412
|
+
text: JSON.stringify(full.modelDefinition, null, 2),
|
|
1413
|
+
},
|
|
1414
|
+
],
|
|
1415
|
+
};
|
|
1416
|
+
});
|
|
1417
|
+
server.tool("create_model", "Create a new Custom Data Model (ModelDefinition). Returns a minimal ack by default; pass response='full' for the full model.", {
|
|
1418
|
+
name: z.string().describe("Display name"),
|
|
1419
|
+
slug: z
|
|
1420
|
+
.string()
|
|
1421
|
+
.trim()
|
|
1422
|
+
.regex(SLUG_RE, SLUG_MSG)
|
|
1423
|
+
.describe("URL-safe slug, lowercase (regex: ^[a-z][a-z0-9-]*$)"),
|
|
1424
|
+
description: z.string().optional(),
|
|
1425
|
+
icon: z
|
|
1426
|
+
.string()
|
|
1427
|
+
.optional()
|
|
1428
|
+
.describe("Lucide icon name, defaults to 'database'"),
|
|
1429
|
+
color: z.string().optional(),
|
|
1430
|
+
displayField: z
|
|
1431
|
+
.string()
|
|
1432
|
+
.optional()
|
|
1433
|
+
.describe("Field key used as the record's display label in UI"),
|
|
1434
|
+
defaultSort: z.preprocess(jsonPreprocess, defaultSortSchema).optional(),
|
|
1435
|
+
fields: z
|
|
1436
|
+
.preprocess(jsonPreprocess, z.array(propertyFieldSchema))
|
|
1437
|
+
.optional()
|
|
1438
|
+
.describe("Field definitions. Validates record data on write."),
|
|
1439
|
+
statusField: z
|
|
1440
|
+
.preprocess(jsonPreprocess, statusFieldSchema)
|
|
1441
|
+
.optional()
|
|
1442
|
+
.describe("Enable record lifecycle states with allowed transitions"),
|
|
1443
|
+
response: responseModeSchema,
|
|
1444
|
+
}, async ({ name, slug, description, icon, color, displayField, defaultSort, fields, statusField, response, }) => {
|
|
1445
|
+
const input = { name, slug };
|
|
1446
|
+
if (description !== undefined)
|
|
1447
|
+
input.description = description;
|
|
1448
|
+
if (icon !== undefined)
|
|
1449
|
+
input.icon = icon;
|
|
1450
|
+
if (color !== undefined)
|
|
1451
|
+
input.color = color;
|
|
1452
|
+
if (displayField !== undefined)
|
|
1453
|
+
input.displayField = displayField;
|
|
1454
|
+
if (defaultSort !== undefined)
|
|
1455
|
+
input.defaultSort = defaultSort;
|
|
1456
|
+
if (fields !== undefined)
|
|
1457
|
+
input.fields = fields;
|
|
1458
|
+
if (statusField !== undefined)
|
|
1459
|
+
input.statusField = statusField;
|
|
1460
|
+
const data = await client.query(CREATE_MODEL_DEFINITION_MUTATION, { input });
|
|
1461
|
+
return jsonText(response, data.createModelDefinition, modelMinimal);
|
|
1462
|
+
});
|
|
1463
|
+
server.tool("update_model", "Update any field of an existing ModelDefinition. Changing 'fields' migrates the schema - existing records are re-validated on next write. Returns a minimal ack by default; pass response='full' for the full model.", {
|
|
1464
|
+
id: z.string().describe("Model id (ObjectId) to update"),
|
|
1465
|
+
name: z.string().optional(),
|
|
1466
|
+
slug: z.string().trim().regex(SLUG_RE, SLUG_MSG).optional(),
|
|
1467
|
+
description: z.string().optional(),
|
|
1468
|
+
icon: z.string().optional(),
|
|
1469
|
+
color: z.string().optional(),
|
|
1470
|
+
displayField: z.string().optional(),
|
|
1471
|
+
defaultSort: z.preprocess(jsonPreprocess, defaultSortSchema).optional(),
|
|
1472
|
+
fields: z
|
|
1473
|
+
.preprocess(jsonPreprocess, z.array(propertyFieldSchema))
|
|
1474
|
+
.optional(),
|
|
1475
|
+
statusField: z.preprocess(jsonPreprocess, statusFieldSchema).optional(),
|
|
1476
|
+
response: responseModeSchema,
|
|
1477
|
+
}, async (args) => {
|
|
1478
|
+
const input = { id: args.id };
|
|
1479
|
+
for (const key of [
|
|
1480
|
+
"name",
|
|
1481
|
+
"slug",
|
|
1482
|
+
"description",
|
|
1483
|
+
"icon",
|
|
1484
|
+
"color",
|
|
1485
|
+
"displayField",
|
|
1486
|
+
"defaultSort",
|
|
1487
|
+
"fields",
|
|
1488
|
+
"statusField",
|
|
1489
|
+
]) {
|
|
1490
|
+
if (args[key] !== undefined)
|
|
1491
|
+
input[key] = args[key];
|
|
1492
|
+
}
|
|
1493
|
+
const data = await client.query(UPDATE_MODEL_DEFINITION_MUTATION, { input });
|
|
1494
|
+
if (!data.updateModelDefinition) {
|
|
1495
|
+
return {
|
|
1496
|
+
content: [{ type: "text", text: "Model not found" }],
|
|
1497
|
+
isError: true,
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
return jsonText(args.response, data.updateModelDefinition, modelMinimal);
|
|
1501
|
+
});
|
|
1502
|
+
server.tool("delete_model", "Delete a ModelDefinition. WARNING: cascades - ALL records of this model are deleted.", {
|
|
1503
|
+
id: z.string().describe("Model id (ObjectId) to delete"),
|
|
1504
|
+
}, async ({ id }) => {
|
|
1505
|
+
const data = await client.query(DELETE_MODEL_DEFINITION_MUTATION, { id });
|
|
1506
|
+
return {
|
|
1507
|
+
content: [
|
|
1508
|
+
{
|
|
1509
|
+
type: "text",
|
|
1510
|
+
text: data.deleteModelDefinition
|
|
1511
|
+
? "Model deleted (records cascaded)"
|
|
1512
|
+
: "Failed to delete model",
|
|
1513
|
+
},
|
|
1514
|
+
],
|
|
1515
|
+
isError: !data.deleteModelDefinition,
|
|
1516
|
+
};
|
|
1517
|
+
});
|
|
1518
|
+
server.tool("list_records", "List records of a model with filter, sort, and pagination. Filter is a MongoDB-style plain object. Sort is a string (e.g. '-createdAt' for desc). Populate loads relations by field key.", {
|
|
1519
|
+
modelId: z.string().describe("Target model id (ObjectId)"),
|
|
1520
|
+
filter: z
|
|
1521
|
+
.preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
|
|
1522
|
+
.optional()
|
|
1523
|
+
.describe("Plain object filter, e.g. {status: 'active'}"),
|
|
1524
|
+
limit: z.number().int().optional().default(50),
|
|
1525
|
+
offset: z.number().int().optional().default(0),
|
|
1526
|
+
sort: z
|
|
1527
|
+
.string()
|
|
1528
|
+
.optional()
|
|
1529
|
+
.describe("Sort expression, e.g. 'createdAt' or '-updatedAt'"),
|
|
1530
|
+
populate: z
|
|
1531
|
+
.array(z.string())
|
|
1532
|
+
.optional()
|
|
1533
|
+
.describe("Field keys of type=relation to populate"),
|
|
1534
|
+
}, async ({ modelId, filter, limit, offset, sort, populate }) => {
|
|
1535
|
+
const data = await client.query(MODEL_RECORDS_QUERY, {
|
|
1536
|
+
modelId,
|
|
1537
|
+
filter,
|
|
1538
|
+
limit,
|
|
1539
|
+
offset,
|
|
1540
|
+
sort,
|
|
1541
|
+
populate,
|
|
1542
|
+
});
|
|
1543
|
+
return {
|
|
1544
|
+
content: [
|
|
1545
|
+
{
|
|
1546
|
+
type: "text",
|
|
1547
|
+
text: JSON.stringify(data.modelRecords, null, 2),
|
|
1548
|
+
},
|
|
1549
|
+
],
|
|
1550
|
+
};
|
|
1551
|
+
});
|
|
1552
|
+
server.tool("get_record", "Get a single record by id.", {
|
|
1553
|
+
id: z.string().describe("Record id (ObjectId)"),
|
|
1554
|
+
}, async ({ id }) => {
|
|
1555
|
+
const data = await client.query(MODEL_RECORD_BY_ID_QUERY, { id });
|
|
1556
|
+
if (!data.modelRecord) {
|
|
1557
|
+
return {
|
|
1558
|
+
content: [{ type: "text", text: "Record not found" }],
|
|
1559
|
+
isError: true,
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
content: [
|
|
1564
|
+
{
|
|
1565
|
+
type: "text",
|
|
1566
|
+
text: JSON.stringify(data.modelRecord, null, 2),
|
|
1567
|
+
},
|
|
1568
|
+
],
|
|
1569
|
+
};
|
|
1570
|
+
});
|
|
1571
|
+
server.tool("create_record", "Create a new record in a model. Data keys must match field keys of the model; backend validates against the ModelDefinition. Returns a minimal ack by default; pass response='full' for the full record.", {
|
|
1572
|
+
modelId: z.string().describe("Target model id (ObjectId)"),
|
|
1573
|
+
data: z
|
|
1574
|
+
.preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
|
|
1575
|
+
.describe("Record data keyed by model field keys"),
|
|
1576
|
+
response: responseModeSchema,
|
|
1577
|
+
}, async ({ modelId, data: recordData, response }) => {
|
|
1578
|
+
const result = await client.query(CREATE_MODEL_RECORD_MUTATION, {
|
|
1579
|
+
input: { modelId, data: recordData },
|
|
1580
|
+
});
|
|
1581
|
+
return jsonText(response, result.createModelRecord, recordMinimal);
|
|
1582
|
+
});
|
|
1583
|
+
server.tool("update_record", "Update a record. Pass 'data' for a full data replace. Pass 'status' to transition the record's status (validated against statusField.transitions). At least one of data/status is required. Returns a minimal ack by default; pass response='full' for the full record.", {
|
|
1584
|
+
id: z.string().describe("Record id (ObjectId)"),
|
|
1585
|
+
data: z
|
|
1586
|
+
.preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
|
|
1587
|
+
.optional()
|
|
1588
|
+
.describe("New data (full replace)"),
|
|
1589
|
+
status: z
|
|
1590
|
+
.string()
|
|
1591
|
+
.optional()
|
|
1592
|
+
.describe("New status value (must be allowed by model.statusField)"),
|
|
1593
|
+
response: responseModeSchema,
|
|
1594
|
+
}, async ({ id, data: recordData, status, response }) => {
|
|
1595
|
+
if (recordData === undefined && status === undefined) {
|
|
1596
|
+
return {
|
|
1597
|
+
content: [
|
|
1598
|
+
{
|
|
1599
|
+
type: "text",
|
|
1600
|
+
text: "Provide 'data' and/or 'status' to update",
|
|
1601
|
+
},
|
|
1602
|
+
],
|
|
1603
|
+
isError: true,
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
let dataResult = null;
|
|
1607
|
+
let statusResult = null;
|
|
1608
|
+
if (recordData !== undefined) {
|
|
1609
|
+
const res = await client.query(UPDATE_MODEL_RECORD_MUTATION, {
|
|
1610
|
+
input: { id, data: recordData },
|
|
1611
|
+
});
|
|
1612
|
+
dataResult = res.updateModelRecord;
|
|
1613
|
+
if (!dataResult) {
|
|
1614
|
+
return {
|
|
1615
|
+
content: [{ type: "text", text: "Record not found" }],
|
|
1616
|
+
isError: true,
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (status !== undefined) {
|
|
1621
|
+
// Data update (if any) already committed. Wrap status call so a
|
|
1622
|
+
// failed transition reports partial-success clearly instead of
|
|
1623
|
+
// hiding the completed data write behind a bare throw.
|
|
1624
|
+
try {
|
|
1625
|
+
const res = await client.query(UPDATE_MODEL_RECORD_STATUS_MUTATION, {
|
|
1626
|
+
input: { id, status },
|
|
1627
|
+
});
|
|
1628
|
+
statusResult = res.updateModelRecordStatus;
|
|
1629
|
+
if (!statusResult) {
|
|
1630
|
+
if (dataResult) {
|
|
1631
|
+
return {
|
|
1632
|
+
content: [
|
|
1633
|
+
{
|
|
1634
|
+
type: "text",
|
|
1635
|
+
text: JSON.stringify({
|
|
1636
|
+
partialSuccess: true,
|
|
1637
|
+
message: "Data updated, but record not found when applying status.",
|
|
1638
|
+
dataResult,
|
|
1639
|
+
}, null, 2),
|
|
1640
|
+
},
|
|
1641
|
+
],
|
|
1642
|
+
isError: true,
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
content: [{ type: "text", text: "Record not found" }],
|
|
1647
|
+
isError: true,
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
catch (error) {
|
|
1652
|
+
if (dataResult) {
|
|
1653
|
+
return {
|
|
1654
|
+
content: [
|
|
1655
|
+
{
|
|
1656
|
+
type: "text",
|
|
1657
|
+
text: JSON.stringify({
|
|
1658
|
+
partialSuccess: true,
|
|
1659
|
+
message: "Data updated, but status transition failed. Retry status only.",
|
|
1660
|
+
dataResult,
|
|
1661
|
+
statusError: error instanceof Error ? error.message : String(error),
|
|
1662
|
+
}, null, 2),
|
|
1663
|
+
},
|
|
1664
|
+
],
|
|
1665
|
+
isError: true,
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
throw error;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const finalResult = (statusResult ?? dataResult);
|
|
1672
|
+
return jsonText(response, finalResult, recordMinimal);
|
|
1673
|
+
});
|
|
1674
|
+
server.tool("delete_record", "Delete a record permanently.", {
|
|
1675
|
+
id: z.string().describe("Record id (ObjectId) to delete"),
|
|
1676
|
+
}, async ({ id }) => {
|
|
1677
|
+
const data = await client.query(DELETE_MODEL_RECORD_MUTATION, { id });
|
|
1678
|
+
return {
|
|
1679
|
+
content: [
|
|
1680
|
+
{
|
|
1681
|
+
type: "text",
|
|
1682
|
+
text: data.deleteModelRecord
|
|
1683
|
+
? "Record deleted"
|
|
1684
|
+
: "Failed to delete record",
|
|
1685
|
+
},
|
|
1686
|
+
],
|
|
1687
|
+
isError: !data.deleteModelRecord,
|
|
1688
|
+
};
|
|
1689
|
+
});
|
|
1690
|
+
server.tool("import_records", "Bulk import records into a model. Accepts up to 1000 rows as plain objects. Returns { importedCount, errors[{row, message}] } - same shape as CSV/XLSX import.", {
|
|
1691
|
+
modelId: z.string().describe("Target model id (ObjectId)"),
|
|
1692
|
+
rows: z
|
|
1693
|
+
.preprocess(jsonPreprocess, z
|
|
1694
|
+
.array(z.record(z.string(), z.unknown()))
|
|
1695
|
+
.min(1, "Provide at least 1 row")
|
|
1696
|
+
.max(1000, "Maximum 1000 rows per import"))
|
|
1697
|
+
.describe("Array of plain objects keyed by model field keys"),
|
|
1698
|
+
}, async ({ modelId, rows }) => {
|
|
1699
|
+
const data = await client.query(IMPORT_MODEL_RECORDS_MUTATION, { input: { modelId, rows } });
|
|
1700
|
+
return {
|
|
1701
|
+
content: [
|
|
1702
|
+
{
|
|
1703
|
+
type: "text",
|
|
1704
|
+
text: JSON.stringify(data.importModelRecords, null, 2),
|
|
1705
|
+
},
|
|
1706
|
+
],
|
|
1707
|
+
};
|
|
1708
|
+
});
|
|
1709
|
+
server.tool("list_model_templates", "List available model templates (E-commerce, Blog, etc.). Each template installs one or more ModelDefinitions.", {}, async () => {
|
|
1710
|
+
const data = await client.query(MODEL_TEMPLATES_QUERY);
|
|
1711
|
+
return {
|
|
1712
|
+
content: [
|
|
1713
|
+
{
|
|
1714
|
+
type: "text",
|
|
1715
|
+
text: JSON.stringify(data.modelTemplates, null, 2),
|
|
1716
|
+
},
|
|
1717
|
+
],
|
|
1718
|
+
};
|
|
1719
|
+
});
|
|
1720
|
+
server.tool("create_model_from_template", "Install a model template into the workspace. Creates all models defined by the template; skips any whose slug already exists. Returns { templateId, installedCount, skippedSlugs }.", {
|
|
1721
|
+
templateId: z
|
|
1722
|
+
.string()
|
|
1723
|
+
.describe("Template id (from list_model_templates)"),
|
|
1724
|
+
}, async ({ templateId }) => {
|
|
1725
|
+
const data = await client.query(INSTALL_MODEL_TEMPLATE_MUTATION, { templateId });
|
|
1726
|
+
return {
|
|
1727
|
+
content: [
|
|
1728
|
+
{
|
|
1729
|
+
type: "text",
|
|
1730
|
+
text: JSON.stringify(data.installModelTemplate, null, 2),
|
|
1731
|
+
},
|
|
1732
|
+
],
|
|
1733
|
+
};
|
|
1734
|
+
});
|
|
1314
1735
|
// ─── Resources ───────────────────────────────────────────────
|
|
1315
1736
|
server.resource("sitemap", "cmssy://sitemap", {
|
|
1316
1737
|
description: "Full page tree as JSON — all pages with hierarchy",
|