@cmssy/mcp-server 0.5.0 → 0.6.0
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 +20 -5
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +205 -0
- package/dist/queries.js.map +1 -1
- package/dist/responses.d.ts +70 -0
- package/dist/responses.d.ts.map +1 -0
- package/dist/responses.js +75 -0
- package/dist/responses.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +555 -143
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
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 {
|
|
@@ -429,16 +416,12 @@ export function createServer(client) {
|
|
|
429
416
|
blockVersion: b.blockVersion,
|
|
430
417
|
}));
|
|
431
418
|
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
|
-
};
|
|
419
|
+
return jsonText(response, data.publishPage, (p) => pageMinimal(p, { published: true }));
|
|
440
420
|
});
|
|
441
|
-
server.tool("unpublish_page", "Unpublish a published page (toggles published state off).
|
|
421
|
+
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.", {
|
|
422
|
+
pageId: z.string().describe("Page ID to unpublish"),
|
|
423
|
+
response: responseModeSchema,
|
|
424
|
+
}, async ({ pageId, response }) => {
|
|
442
425
|
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
443
426
|
if (!pageData.page) {
|
|
444
427
|
return {
|
|
@@ -454,16 +437,12 @@ export function createServer(client) {
|
|
|
454
437
|
};
|
|
455
438
|
}
|
|
456
439
|
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
|
-
};
|
|
440
|
+
return jsonText(response, data.togglePublish, (p) => pageMinimal(p, { published: false }));
|
|
465
441
|
});
|
|
466
|
-
server.tool("revert_to_published", "Discard all draft changes and revert a page to its last published version.
|
|
442
|
+
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.", {
|
|
443
|
+
pageId: z.string().describe("Page ID to revert"),
|
|
444
|
+
response: responseModeSchema,
|
|
445
|
+
}, async ({ pageId, response }) => {
|
|
467
446
|
const data = await client.query(REVERT_TO_PUBLISHED_MUTATION, { id: pageId });
|
|
468
447
|
if (!data.revertToPublished) {
|
|
469
448
|
return {
|
|
@@ -476,14 +455,7 @@ export function createServer(client) {
|
|
|
476
455
|
isError: true,
|
|
477
456
|
};
|
|
478
457
|
}
|
|
479
|
-
return
|
|
480
|
-
content: [
|
|
481
|
-
{
|
|
482
|
-
type: "text",
|
|
483
|
-
text: JSON.stringify(data.revertToPublished, null, 2),
|
|
484
|
-
},
|
|
485
|
-
],
|
|
486
|
-
};
|
|
458
|
+
return jsonText(response, data.revertToPublished, pageMinimal);
|
|
487
459
|
});
|
|
488
460
|
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
461
|
const data = await client.query(REMOVE_PAGE_MUTATION, { id: pageId });
|
|
@@ -499,7 +471,7 @@ export function createServer(client) {
|
|
|
499
471
|
};
|
|
500
472
|
});
|
|
501
473
|
// ─── 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.", {
|
|
474
|
+
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
475
|
pageId: z.string().describe("Page ID"),
|
|
504
476
|
layoutBlocks: z
|
|
505
477
|
.array(z.record(z.string(), z.unknown()))
|
|
@@ -517,7 +489,8 @@ export function createServer(client) {
|
|
|
517
489
|
.boolean()
|
|
518
490
|
.optional()
|
|
519
491
|
.describe("Whether this page inherits layout from parent"),
|
|
520
|
-
|
|
492
|
+
response: responseModeSchema,
|
|
493
|
+
}, async ({ pageId, layoutBlocks, layoutOverrides, inheritsLayout, response, }) => {
|
|
521
494
|
// Validate block types against workspace registry
|
|
522
495
|
if (layoutBlocks) {
|
|
523
496
|
const types = layoutBlocks
|
|
@@ -568,17 +541,10 @@ export function createServer(client) {
|
|
|
568
541
|
isError: true,
|
|
569
542
|
};
|
|
570
543
|
}
|
|
571
|
-
return
|
|
572
|
-
content: [
|
|
573
|
-
{
|
|
574
|
-
type: "text",
|
|
575
|
-
text: JSON.stringify(data.updatePageLayout, null, 2),
|
|
576
|
-
},
|
|
577
|
-
],
|
|
578
|
-
};
|
|
544
|
+
return jsonText(response, data.updatePageLayout, pageMinimal);
|
|
579
545
|
});
|
|
580
546
|
// ─── 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.", {
|
|
547
|
+
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
548
|
pageId: z.string().describe("Page ID"),
|
|
583
549
|
block: z.preprocess(jsonPreprocess, z.object({
|
|
584
550
|
type: z
|
|
@@ -594,7 +560,8 @@ export function createServer(client) {
|
|
|
594
560
|
.number()
|
|
595
561
|
.optional()
|
|
596
562
|
.describe("0-based position to insert at (default: end)"),
|
|
597
|
-
|
|
563
|
+
response: responseModeSchema,
|
|
564
|
+
}, async ({ pageId, block, position, response }) => {
|
|
598
565
|
// Validate block type against workspace registry
|
|
599
566
|
const blockDef = await findBlockDef(block.type);
|
|
600
567
|
if (!blockDef) {
|
|
@@ -654,14 +621,18 @@ export function createServer(client) {
|
|
|
654
621
|
};
|
|
655
622
|
const layoutBlocks = [...existingLayoutBlocks, newLayoutBlock];
|
|
656
623
|
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks } });
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
624
|
+
if (!data.updatePageLayout) {
|
|
625
|
+
return {
|
|
626
|
+
content: [
|
|
627
|
+
{
|
|
628
|
+
type: "text",
|
|
629
|
+
text: `Failed to update layout while adding block to page '${pageId}'`,
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
isError: true,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
return jsonText(response, { blockId: newBlockId, page: data.updatePageLayout }, () => pageBlockMinimal(data.updatePageLayout, newBlockId));
|
|
665
636
|
}
|
|
666
637
|
else {
|
|
667
638
|
// Content block — add to blocks via savePage
|
|
@@ -692,24 +663,18 @@ export function createServer(client) {
|
|
|
692
663
|
blocks,
|
|
693
664
|
},
|
|
694
665
|
});
|
|
695
|
-
return {
|
|
696
|
-
content: [
|
|
697
|
-
{
|
|
698
|
-
type: "text",
|
|
699
|
-
text: JSON.stringify({ blockId: newBlockId, page: data.savePage }, null, 2),
|
|
700
|
-
},
|
|
701
|
-
],
|
|
702
|
-
};
|
|
666
|
+
return jsonText(response, { blockId: newBlockId, page: data.savePage }, () => pageBlockMinimal(data.savePage, newBlockId));
|
|
703
667
|
}
|
|
704
668
|
});
|
|
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.", {
|
|
669
|
+
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
670
|
pageId: z.string().describe("Page ID"),
|
|
707
671
|
blockId: z.string().describe("Block instance ID (UUID) to update"),
|
|
708
672
|
content: z
|
|
709
673
|
.record(z.string(), z.unknown())
|
|
710
674
|
.describe("Content to merge: { en: { title: 'New Title' } }"),
|
|
711
675
|
settings: z.record(z.string(), z.unknown()).optional(),
|
|
712
|
-
|
|
676
|
+
response: responseModeSchema,
|
|
677
|
+
}, async ({ pageId, blockId, content, settings, response }) => {
|
|
713
678
|
// Fetch current page
|
|
714
679
|
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
715
680
|
if (!pageData.page) {
|
|
@@ -775,14 +740,18 @@ export function createServer(client) {
|
|
|
775
740
|
targetArray[targetIndex] = existingBlock;
|
|
776
741
|
if (isLayout) {
|
|
777
742
|
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks: targetArray } });
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
743
|
+
if (!data.updatePageLayout) {
|
|
744
|
+
return {
|
|
745
|
+
content: [
|
|
746
|
+
{
|
|
747
|
+
type: "text",
|
|
748
|
+
text: `Failed to update layout while editing block '${blockId}' on page '${pageId}'`,
|
|
749
|
+
},
|
|
750
|
+
],
|
|
751
|
+
isError: true,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
return jsonText(response, data.updatePageLayout, (p) => pageBlockMinimal(p, blockId));
|
|
786
755
|
}
|
|
787
756
|
else {
|
|
788
757
|
const data = await client.query(SAVE_PAGE_MUTATION, {
|
|
@@ -794,14 +763,7 @@ export function createServer(client) {
|
|
|
794
763
|
blocks: targetArray,
|
|
795
764
|
},
|
|
796
765
|
});
|
|
797
|
-
return
|
|
798
|
-
content: [
|
|
799
|
-
{
|
|
800
|
-
type: "text",
|
|
801
|
-
text: JSON.stringify(data.savePage, null, 2),
|
|
802
|
-
},
|
|
803
|
-
],
|
|
804
|
-
};
|
|
766
|
+
return jsonText(response, data.savePage, (p) => pageBlockMinimal(p, blockId));
|
|
805
767
|
}
|
|
806
768
|
});
|
|
807
769
|
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 +860,22 @@ export function createServer(client) {
|
|
|
898
860
|
isError: true,
|
|
899
861
|
};
|
|
900
862
|
}
|
|
863
|
+
// Compact JSON: patch_block_content is ack-only (no pretty-print
|
|
864
|
+
// needed), matching the compact shape used by minimal-mode returns.
|
|
901
865
|
return {
|
|
902
866
|
content: [
|
|
903
867
|
{
|
|
904
868
|
type: "text",
|
|
905
|
-
text: JSON.stringify(data.patchBlockContent
|
|
869
|
+
text: JSON.stringify(data.patchBlockContent),
|
|
906
870
|
},
|
|
907
871
|
],
|
|
908
872
|
};
|
|
909
873
|
});
|
|
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.", {
|
|
874
|
+
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
875
|
pageId: z.string().describe("Page ID"),
|
|
912
876
|
blockId: z.string().describe("Block instance ID (UUID) to remove"),
|
|
913
|
-
|
|
877
|
+
response: responseModeSchema,
|
|
878
|
+
}, async ({ pageId, blockId, response }) => {
|
|
914
879
|
const pageData = await client.query(PAGE_BY_ID_QUERY, { pageId });
|
|
915
880
|
if (!pageData.page) {
|
|
916
881
|
return {
|
|
@@ -931,27 +896,24 @@ export function createServer(client) {
|
|
|
931
896
|
blocks: contentBlocks,
|
|
932
897
|
},
|
|
933
898
|
});
|
|
934
|
-
return
|
|
935
|
-
content: [
|
|
936
|
-
{
|
|
937
|
-
type: "text",
|
|
938
|
-
text: JSON.stringify(data.savePage, null, 2),
|
|
939
|
-
},
|
|
940
|
-
],
|
|
941
|
-
};
|
|
899
|
+
return jsonText(response, data.savePage, (p) => pageBlockMinimal(p, blockId));
|
|
942
900
|
}
|
|
943
901
|
// Try layout blocks
|
|
944
902
|
const layoutBlocks = (page.layoutBlocks || []).filter((b) => b.id !== blockId);
|
|
945
903
|
if (layoutBlocks.length < (page.layoutBlocks || []).length) {
|
|
946
904
|
const data = await client.query(UPDATE_PAGE_LAYOUT_MUTATION, { input: { pageId, layoutBlocks } });
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
905
|
+
if (!data.updatePageLayout) {
|
|
906
|
+
return {
|
|
907
|
+
content: [
|
|
908
|
+
{
|
|
909
|
+
type: "text",
|
|
910
|
+
text: `Failed to update layout while removing block '${blockId}' from page '${pageId}'`,
|
|
911
|
+
},
|
|
912
|
+
],
|
|
913
|
+
isError: true,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
return jsonText(response, data.updatePageLayout, (p) => pageBlockMinimal(p, blockId));
|
|
955
917
|
}
|
|
956
918
|
return {
|
|
957
919
|
content: [
|
|
@@ -1007,7 +969,7 @@ export function createServer(client) {
|
|
|
1007
969
|
],
|
|
1008
970
|
};
|
|
1009
971
|
});
|
|
1010
|
-
server.tool("create_form", "Create a new form with fields and settings. Returns the
|
|
972
|
+
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
973
|
name: z.string().describe("Form name"),
|
|
1012
974
|
slug: z.string().describe("URL-friendly slug (must be unique)"),
|
|
1013
975
|
description: z.string().optional().describe("Form description"),
|
|
@@ -1090,7 +1052,8 @@ export function createServer(client) {
|
|
|
1090
1052
|
}))
|
|
1091
1053
|
.optional()
|
|
1092
1054
|
.describe("Form settings (action type, notifications, etc.)"),
|
|
1093
|
-
|
|
1055
|
+
response: responseModeSchema,
|
|
1056
|
+
}, async ({ name, slug, description, fields, settings, response }) => {
|
|
1094
1057
|
const input = { name, slug };
|
|
1095
1058
|
if (description)
|
|
1096
1059
|
input.description = description;
|
|
@@ -1099,16 +1062,9 @@ export function createServer(client) {
|
|
|
1099
1062
|
if (settings)
|
|
1100
1063
|
input.settings = settings;
|
|
1101
1064
|
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
|
-
};
|
|
1065
|
+
return jsonText(response, data.createForm, formMinimal);
|
|
1110
1066
|
});
|
|
1111
|
-
server.tool("update_form", "Update an existing form's name, slug, status, fields, or settings.", {
|
|
1067
|
+
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
1068
|
formId: z.string().describe("Form ID to update"),
|
|
1113
1069
|
name: z.string().optional().describe("New form name"),
|
|
1114
1070
|
slug: z.string().optional().describe("New slug"),
|
|
@@ -1188,7 +1144,8 @@ export function createServer(client) {
|
|
|
1188
1144
|
}))
|
|
1189
1145
|
.optional()
|
|
1190
1146
|
.describe("Updated settings"),
|
|
1191
|
-
|
|
1147
|
+
response: responseModeSchema,
|
|
1148
|
+
}, async ({ formId, name, slug, description, status, fields, settings, response, }) => {
|
|
1192
1149
|
const input = {};
|
|
1193
1150
|
if (name !== undefined)
|
|
1194
1151
|
input.name = name;
|
|
@@ -1211,14 +1168,7 @@ export function createServer(client) {
|
|
|
1211
1168
|
isError: true,
|
|
1212
1169
|
};
|
|
1213
1170
|
}
|
|
1214
|
-
return
|
|
1215
|
-
content: [
|
|
1216
|
-
{
|
|
1217
|
-
type: "text",
|
|
1218
|
-
text: JSON.stringify(data.updateForm, null, 2),
|
|
1219
|
-
},
|
|
1220
|
-
],
|
|
1221
|
-
};
|
|
1171
|
+
return jsonText(response, data.updateForm, formMinimal);
|
|
1222
1172
|
});
|
|
1223
1173
|
server.tool("delete_form", "Permanently delete a form and all its submissions.", {
|
|
1224
1174
|
formId: z.string().describe("Form ID to delete"),
|
|
@@ -1311,6 +1261,468 @@ export function createServer(client) {
|
|
|
1311
1261
|
],
|
|
1312
1262
|
};
|
|
1313
1263
|
});
|
|
1264
|
+
// ─── Model Tools (Custom Data Models) ────────────────────────
|
|
1265
|
+
const propertyFieldTypes = [
|
|
1266
|
+
"string",
|
|
1267
|
+
"richtext",
|
|
1268
|
+
"number",
|
|
1269
|
+
"boolean",
|
|
1270
|
+
"date",
|
|
1271
|
+
"datetime",
|
|
1272
|
+
"email",
|
|
1273
|
+
"url",
|
|
1274
|
+
"media",
|
|
1275
|
+
"relation",
|
|
1276
|
+
"select",
|
|
1277
|
+
"multiselect",
|
|
1278
|
+
"object",
|
|
1279
|
+
"list",
|
|
1280
|
+
];
|
|
1281
|
+
const relationTypes = ["hasOne", "hasMany", "manyToMany"];
|
|
1282
|
+
const propertyFieldSchema = z.lazy(() => z.object({
|
|
1283
|
+
key: z.string().describe("Field key (identifier in record data)"),
|
|
1284
|
+
label: z.string().describe("Human-readable field label"),
|
|
1285
|
+
type: z
|
|
1286
|
+
.enum(propertyFieldTypes)
|
|
1287
|
+
.describe("Field type — controls validation + UI"),
|
|
1288
|
+
required: z.boolean().optional(),
|
|
1289
|
+
description: z.string().optional(),
|
|
1290
|
+
defaultValue: z.string().optional(),
|
|
1291
|
+
options: z
|
|
1292
|
+
.array(z.string())
|
|
1293
|
+
.optional()
|
|
1294
|
+
.describe("For select/multiselect: allowed values"),
|
|
1295
|
+
fields: z
|
|
1296
|
+
.array(propertyFieldSchema)
|
|
1297
|
+
.optional()
|
|
1298
|
+
.describe("For type=object: nested fields"),
|
|
1299
|
+
itemType: z
|
|
1300
|
+
.enum(propertyFieldTypes)
|
|
1301
|
+
.optional()
|
|
1302
|
+
.describe("For type=list: item type"),
|
|
1303
|
+
itemFields: z
|
|
1304
|
+
.array(propertyFieldSchema)
|
|
1305
|
+
.optional()
|
|
1306
|
+
.describe("For type=list of objects: nested field defs"),
|
|
1307
|
+
relationTo: z
|
|
1308
|
+
.string()
|
|
1309
|
+
.optional()
|
|
1310
|
+
.describe("For type=relation: target model slug"),
|
|
1311
|
+
relationType: z.enum(relationTypes).optional(),
|
|
1312
|
+
acceptedTypes: z
|
|
1313
|
+
.array(z.enum(mediaTypeValues))
|
|
1314
|
+
.optional()
|
|
1315
|
+
.describe("For type=media: allowed media kinds (empty = all)"),
|
|
1316
|
+
multiple: z
|
|
1317
|
+
.boolean()
|
|
1318
|
+
.optional()
|
|
1319
|
+
.describe("For type=media: true = gallery, false = single"),
|
|
1320
|
+
schemaProperty: z.string().optional(),
|
|
1321
|
+
minLength: z.number().int().optional(),
|
|
1322
|
+
maxLength: z.number().int().optional(),
|
|
1323
|
+
minValue: z.number().optional(),
|
|
1324
|
+
maxValue: z.number().optional(),
|
|
1325
|
+
pattern: z.string().optional(),
|
|
1326
|
+
}));
|
|
1327
|
+
const statusFieldSchema = z.object({
|
|
1328
|
+
enabled: z.boolean().optional(),
|
|
1329
|
+
values: z
|
|
1330
|
+
.array(z.string())
|
|
1331
|
+
.optional()
|
|
1332
|
+
.describe("Allowed status values (e.g. ['draft','active'])"),
|
|
1333
|
+
defaultValue: z.string().optional(),
|
|
1334
|
+
transitions: z
|
|
1335
|
+
.array(z.object({
|
|
1336
|
+
from: z.string(),
|
|
1337
|
+
to: z.array(z.string()),
|
|
1338
|
+
}))
|
|
1339
|
+
.optional()
|
|
1340
|
+
.describe("Allowed transitions: from one state to a set of states"),
|
|
1341
|
+
});
|
|
1342
|
+
const defaultSortSchema = z.object({
|
|
1343
|
+
field: z.string(),
|
|
1344
|
+
direction: z.enum(["asc", "desc"]),
|
|
1345
|
+
});
|
|
1346
|
+
const OBJECT_ID_RE = /^[a-f0-9]{24}$/i;
|
|
1347
|
+
// Match backend: apps/backend/src/models/model-definition.ts
|
|
1348
|
+
const SLUG_RE = /^[a-z][a-z0-9-]*$/;
|
|
1349
|
+
const SLUG_MSG = "Slug must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens";
|
|
1350
|
+
server.tool("list_models", "List all Custom Data Models (ModelDefinition) in the workspace.", {}, async () => {
|
|
1351
|
+
const data = await client.query(MODEL_DEFINITIONS_QUERY);
|
|
1352
|
+
return {
|
|
1353
|
+
content: [
|
|
1354
|
+
{
|
|
1355
|
+
type: "text",
|
|
1356
|
+
text: JSON.stringify(data.modelDefinitions, null, 2),
|
|
1357
|
+
},
|
|
1358
|
+
],
|
|
1359
|
+
};
|
|
1360
|
+
});
|
|
1361
|
+
server.tool("get_model", "Get a ModelDefinition by id (24-hex ObjectId) or by slug. Returns full definition including fields, statusField, and defaultSort.", {
|
|
1362
|
+
idOrSlug: z.string().describe("Model id (ObjectId) or slug"),
|
|
1363
|
+
}, async ({ idOrSlug }) => {
|
|
1364
|
+
if (OBJECT_ID_RE.test(idOrSlug)) {
|
|
1365
|
+
const data = await client.query(MODEL_DEFINITION_BY_ID_QUERY, { id: idOrSlug });
|
|
1366
|
+
if (data.modelDefinition) {
|
|
1367
|
+
return {
|
|
1368
|
+
content: [
|
|
1369
|
+
{
|
|
1370
|
+
type: "text",
|
|
1371
|
+
text: JSON.stringify(data.modelDefinition, null, 2),
|
|
1372
|
+
},
|
|
1373
|
+
],
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
// Fallback: resolve slug → id via a lightweight list (id + slug only),
|
|
1378
|
+
// then fetch the full definition for that single model. Avoids
|
|
1379
|
+
// pulling every model's fields when only one is needed.
|
|
1380
|
+
const index = await client.query(MODEL_DEFINITIONS_BY_SLUG_INDEX_QUERY);
|
|
1381
|
+
const match = index.modelDefinitions.find((m) => m.slug === idOrSlug);
|
|
1382
|
+
if (!match) {
|
|
1383
|
+
return {
|
|
1384
|
+
content: [
|
|
1385
|
+
{ type: "text", text: `Model '${idOrSlug}' not found` },
|
|
1386
|
+
],
|
|
1387
|
+
isError: true,
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
const full = await client.query(MODEL_DEFINITION_BY_ID_QUERY, { id: match.id });
|
|
1391
|
+
if (!full.modelDefinition) {
|
|
1392
|
+
return {
|
|
1393
|
+
content: [
|
|
1394
|
+
{ type: "text", text: `Model '${idOrSlug}' not found` },
|
|
1395
|
+
],
|
|
1396
|
+
isError: true,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
return {
|
|
1400
|
+
content: [
|
|
1401
|
+
{
|
|
1402
|
+
type: "text",
|
|
1403
|
+
text: JSON.stringify(full.modelDefinition, null, 2),
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
};
|
|
1407
|
+
});
|
|
1408
|
+
server.tool("create_model", "Create a new Custom Data Model (ModelDefinition). Returns a minimal ack by default; pass response='full' for the full model.", {
|
|
1409
|
+
name: z.string().describe("Display name"),
|
|
1410
|
+
slug: z
|
|
1411
|
+
.string()
|
|
1412
|
+
.trim()
|
|
1413
|
+
.regex(SLUG_RE, SLUG_MSG)
|
|
1414
|
+
.describe("URL-safe slug, lowercase (regex: ^[a-z][a-z0-9-]*$)"),
|
|
1415
|
+
description: z.string().optional(),
|
|
1416
|
+
icon: z
|
|
1417
|
+
.string()
|
|
1418
|
+
.optional()
|
|
1419
|
+
.describe("Lucide icon name, defaults to 'database'"),
|
|
1420
|
+
color: z.string().optional(),
|
|
1421
|
+
displayField: z
|
|
1422
|
+
.string()
|
|
1423
|
+
.optional()
|
|
1424
|
+
.describe("Field key used as the record's display label in UI"),
|
|
1425
|
+
defaultSort: z.preprocess(jsonPreprocess, defaultSortSchema).optional(),
|
|
1426
|
+
fields: z
|
|
1427
|
+
.preprocess(jsonPreprocess, z.array(propertyFieldSchema))
|
|
1428
|
+
.optional()
|
|
1429
|
+
.describe("Field definitions. Validates record data on write."),
|
|
1430
|
+
statusField: z
|
|
1431
|
+
.preprocess(jsonPreprocess, statusFieldSchema)
|
|
1432
|
+
.optional()
|
|
1433
|
+
.describe("Enable record lifecycle states with allowed transitions"),
|
|
1434
|
+
response: responseModeSchema,
|
|
1435
|
+
}, async ({ name, slug, description, icon, color, displayField, defaultSort, fields, statusField, response, }) => {
|
|
1436
|
+
const input = { name, slug };
|
|
1437
|
+
if (description !== undefined)
|
|
1438
|
+
input.description = description;
|
|
1439
|
+
if (icon !== undefined)
|
|
1440
|
+
input.icon = icon;
|
|
1441
|
+
if (color !== undefined)
|
|
1442
|
+
input.color = color;
|
|
1443
|
+
if (displayField !== undefined)
|
|
1444
|
+
input.displayField = displayField;
|
|
1445
|
+
if (defaultSort !== undefined)
|
|
1446
|
+
input.defaultSort = defaultSort;
|
|
1447
|
+
if (fields !== undefined)
|
|
1448
|
+
input.fields = fields;
|
|
1449
|
+
if (statusField !== undefined)
|
|
1450
|
+
input.statusField = statusField;
|
|
1451
|
+
const data = await client.query(CREATE_MODEL_DEFINITION_MUTATION, { input });
|
|
1452
|
+
return jsonText(response, data.createModelDefinition, modelMinimal);
|
|
1453
|
+
});
|
|
1454
|
+
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.", {
|
|
1455
|
+
id: z.string().describe("Model id (ObjectId) to update"),
|
|
1456
|
+
name: z.string().optional(),
|
|
1457
|
+
slug: z.string().trim().regex(SLUG_RE, SLUG_MSG).optional(),
|
|
1458
|
+
description: z.string().optional(),
|
|
1459
|
+
icon: z.string().optional(),
|
|
1460
|
+
color: z.string().optional(),
|
|
1461
|
+
displayField: z.string().optional(),
|
|
1462
|
+
defaultSort: z.preprocess(jsonPreprocess, defaultSortSchema).optional(),
|
|
1463
|
+
fields: z
|
|
1464
|
+
.preprocess(jsonPreprocess, z.array(propertyFieldSchema))
|
|
1465
|
+
.optional(),
|
|
1466
|
+
statusField: z.preprocess(jsonPreprocess, statusFieldSchema).optional(),
|
|
1467
|
+
response: responseModeSchema,
|
|
1468
|
+
}, async (args) => {
|
|
1469
|
+
const input = { id: args.id };
|
|
1470
|
+
for (const key of [
|
|
1471
|
+
"name",
|
|
1472
|
+
"slug",
|
|
1473
|
+
"description",
|
|
1474
|
+
"icon",
|
|
1475
|
+
"color",
|
|
1476
|
+
"displayField",
|
|
1477
|
+
"defaultSort",
|
|
1478
|
+
"fields",
|
|
1479
|
+
"statusField",
|
|
1480
|
+
]) {
|
|
1481
|
+
if (args[key] !== undefined)
|
|
1482
|
+
input[key] = args[key];
|
|
1483
|
+
}
|
|
1484
|
+
const data = await client.query(UPDATE_MODEL_DEFINITION_MUTATION, { input });
|
|
1485
|
+
if (!data.updateModelDefinition) {
|
|
1486
|
+
return {
|
|
1487
|
+
content: [{ type: "text", text: "Model not found" }],
|
|
1488
|
+
isError: true,
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
return jsonText(args.response, data.updateModelDefinition, modelMinimal);
|
|
1492
|
+
});
|
|
1493
|
+
server.tool("delete_model", "Delete a ModelDefinition. WARNING: cascades - ALL records of this model are deleted.", {
|
|
1494
|
+
id: z.string().describe("Model id (ObjectId) to delete"),
|
|
1495
|
+
}, async ({ id }) => {
|
|
1496
|
+
const data = await client.query(DELETE_MODEL_DEFINITION_MUTATION, { id });
|
|
1497
|
+
return {
|
|
1498
|
+
content: [
|
|
1499
|
+
{
|
|
1500
|
+
type: "text",
|
|
1501
|
+
text: data.deleteModelDefinition
|
|
1502
|
+
? "Model deleted (records cascaded)"
|
|
1503
|
+
: "Failed to delete model",
|
|
1504
|
+
},
|
|
1505
|
+
],
|
|
1506
|
+
isError: !data.deleteModelDefinition,
|
|
1507
|
+
};
|
|
1508
|
+
});
|
|
1509
|
+
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.", {
|
|
1510
|
+
modelId: z.string().describe("Target model id (ObjectId)"),
|
|
1511
|
+
filter: z
|
|
1512
|
+
.preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
|
|
1513
|
+
.optional()
|
|
1514
|
+
.describe("Plain object filter, e.g. {status: 'active'}"),
|
|
1515
|
+
limit: z.number().int().optional().default(50),
|
|
1516
|
+
offset: z.number().int().optional().default(0),
|
|
1517
|
+
sort: z
|
|
1518
|
+
.string()
|
|
1519
|
+
.optional()
|
|
1520
|
+
.describe("Sort expression, e.g. 'createdAt' or '-updatedAt'"),
|
|
1521
|
+
populate: z
|
|
1522
|
+
.array(z.string())
|
|
1523
|
+
.optional()
|
|
1524
|
+
.describe("Field keys of type=relation to populate"),
|
|
1525
|
+
}, async ({ modelId, filter, limit, offset, sort, populate }) => {
|
|
1526
|
+
const data = await client.query(MODEL_RECORDS_QUERY, {
|
|
1527
|
+
modelId,
|
|
1528
|
+
filter,
|
|
1529
|
+
limit,
|
|
1530
|
+
offset,
|
|
1531
|
+
sort,
|
|
1532
|
+
populate,
|
|
1533
|
+
});
|
|
1534
|
+
return {
|
|
1535
|
+
content: [
|
|
1536
|
+
{
|
|
1537
|
+
type: "text",
|
|
1538
|
+
text: JSON.stringify(data.modelRecords, null, 2),
|
|
1539
|
+
},
|
|
1540
|
+
],
|
|
1541
|
+
};
|
|
1542
|
+
});
|
|
1543
|
+
server.tool("get_record", "Get a single record by id.", {
|
|
1544
|
+
id: z.string().describe("Record id (ObjectId)"),
|
|
1545
|
+
}, async ({ id }) => {
|
|
1546
|
+
const data = await client.query(MODEL_RECORD_BY_ID_QUERY, { id });
|
|
1547
|
+
if (!data.modelRecord) {
|
|
1548
|
+
return {
|
|
1549
|
+
content: [{ type: "text", text: "Record not found" }],
|
|
1550
|
+
isError: true,
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
return {
|
|
1554
|
+
content: [
|
|
1555
|
+
{
|
|
1556
|
+
type: "text",
|
|
1557
|
+
text: JSON.stringify(data.modelRecord, null, 2),
|
|
1558
|
+
},
|
|
1559
|
+
],
|
|
1560
|
+
};
|
|
1561
|
+
});
|
|
1562
|
+
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.", {
|
|
1563
|
+
modelId: z.string().describe("Target model id (ObjectId)"),
|
|
1564
|
+
data: z
|
|
1565
|
+
.preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
|
|
1566
|
+
.describe("Record data keyed by model field keys"),
|
|
1567
|
+
response: responseModeSchema,
|
|
1568
|
+
}, async ({ modelId, data: recordData, response }) => {
|
|
1569
|
+
const result = await client.query(CREATE_MODEL_RECORD_MUTATION, {
|
|
1570
|
+
input: { modelId, data: recordData },
|
|
1571
|
+
});
|
|
1572
|
+
return jsonText(response, result.createModelRecord, recordMinimal);
|
|
1573
|
+
});
|
|
1574
|
+
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.", {
|
|
1575
|
+
id: z.string().describe("Record id (ObjectId)"),
|
|
1576
|
+
data: z
|
|
1577
|
+
.preprocess(jsonPreprocess, z.record(z.string(), z.unknown()))
|
|
1578
|
+
.optional()
|
|
1579
|
+
.describe("New data (full replace)"),
|
|
1580
|
+
status: z
|
|
1581
|
+
.string()
|
|
1582
|
+
.optional()
|
|
1583
|
+
.describe("New status value (must be allowed by model.statusField)"),
|
|
1584
|
+
response: responseModeSchema,
|
|
1585
|
+
}, async ({ id, data: recordData, status, response }) => {
|
|
1586
|
+
if (recordData === undefined && status === undefined) {
|
|
1587
|
+
return {
|
|
1588
|
+
content: [
|
|
1589
|
+
{
|
|
1590
|
+
type: "text",
|
|
1591
|
+
text: "Provide 'data' and/or 'status' to update",
|
|
1592
|
+
},
|
|
1593
|
+
],
|
|
1594
|
+
isError: true,
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
let dataResult = null;
|
|
1598
|
+
let statusResult = null;
|
|
1599
|
+
if (recordData !== undefined) {
|
|
1600
|
+
const res = await client.query(UPDATE_MODEL_RECORD_MUTATION, {
|
|
1601
|
+
input: { id, data: recordData },
|
|
1602
|
+
});
|
|
1603
|
+
dataResult = res.updateModelRecord;
|
|
1604
|
+
if (!dataResult) {
|
|
1605
|
+
return {
|
|
1606
|
+
content: [{ type: "text", text: "Record not found" }],
|
|
1607
|
+
isError: true,
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
if (status !== undefined) {
|
|
1612
|
+
// Data update (if any) already committed. Wrap status call so a
|
|
1613
|
+
// failed transition reports partial-success clearly instead of
|
|
1614
|
+
// hiding the completed data write behind a bare throw.
|
|
1615
|
+
try {
|
|
1616
|
+
const res = await client.query(UPDATE_MODEL_RECORD_STATUS_MUTATION, {
|
|
1617
|
+
input: { id, status },
|
|
1618
|
+
});
|
|
1619
|
+
statusResult = res.updateModelRecordStatus;
|
|
1620
|
+
if (!statusResult) {
|
|
1621
|
+
if (dataResult) {
|
|
1622
|
+
return {
|
|
1623
|
+
content: [
|
|
1624
|
+
{
|
|
1625
|
+
type: "text",
|
|
1626
|
+
text: JSON.stringify({
|
|
1627
|
+
partialSuccess: true,
|
|
1628
|
+
message: "Data updated, but record not found when applying status.",
|
|
1629
|
+
dataResult,
|
|
1630
|
+
}, null, 2),
|
|
1631
|
+
},
|
|
1632
|
+
],
|
|
1633
|
+
isError: true,
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
return {
|
|
1637
|
+
content: [{ type: "text", text: "Record not found" }],
|
|
1638
|
+
isError: true,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
catch (error) {
|
|
1643
|
+
if (dataResult) {
|
|
1644
|
+
return {
|
|
1645
|
+
content: [
|
|
1646
|
+
{
|
|
1647
|
+
type: "text",
|
|
1648
|
+
text: JSON.stringify({
|
|
1649
|
+
partialSuccess: true,
|
|
1650
|
+
message: "Data updated, but status transition failed. Retry status only.",
|
|
1651
|
+
dataResult,
|
|
1652
|
+
statusError: error instanceof Error ? error.message : String(error),
|
|
1653
|
+
}, null, 2),
|
|
1654
|
+
},
|
|
1655
|
+
],
|
|
1656
|
+
isError: true,
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
throw error;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
const finalResult = (statusResult ?? dataResult);
|
|
1663
|
+
return jsonText(response, finalResult, recordMinimal);
|
|
1664
|
+
});
|
|
1665
|
+
server.tool("delete_record", "Delete a record permanently.", {
|
|
1666
|
+
id: z.string().describe("Record id (ObjectId) to delete"),
|
|
1667
|
+
}, async ({ id }) => {
|
|
1668
|
+
const data = await client.query(DELETE_MODEL_RECORD_MUTATION, { id });
|
|
1669
|
+
return {
|
|
1670
|
+
content: [
|
|
1671
|
+
{
|
|
1672
|
+
type: "text",
|
|
1673
|
+
text: data.deleteModelRecord
|
|
1674
|
+
? "Record deleted"
|
|
1675
|
+
: "Failed to delete record",
|
|
1676
|
+
},
|
|
1677
|
+
],
|
|
1678
|
+
isError: !data.deleteModelRecord,
|
|
1679
|
+
};
|
|
1680
|
+
});
|
|
1681
|
+
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.", {
|
|
1682
|
+
modelId: z.string().describe("Target model id (ObjectId)"),
|
|
1683
|
+
rows: z
|
|
1684
|
+
.preprocess(jsonPreprocess, z
|
|
1685
|
+
.array(z.record(z.string(), z.unknown()))
|
|
1686
|
+
.min(1, "Provide at least 1 row")
|
|
1687
|
+
.max(1000, "Maximum 1000 rows per import"))
|
|
1688
|
+
.describe("Array of plain objects keyed by model field keys"),
|
|
1689
|
+
}, async ({ modelId, rows }) => {
|
|
1690
|
+
const data = await client.query(IMPORT_MODEL_RECORDS_MUTATION, { input: { modelId, rows } });
|
|
1691
|
+
return {
|
|
1692
|
+
content: [
|
|
1693
|
+
{
|
|
1694
|
+
type: "text",
|
|
1695
|
+
text: JSON.stringify(data.importModelRecords, null, 2),
|
|
1696
|
+
},
|
|
1697
|
+
],
|
|
1698
|
+
};
|
|
1699
|
+
});
|
|
1700
|
+
server.tool("list_model_templates", "List available model templates (E-commerce, Blog, etc.). Each template installs one or more ModelDefinitions.", {}, async () => {
|
|
1701
|
+
const data = await client.query(MODEL_TEMPLATES_QUERY);
|
|
1702
|
+
return {
|
|
1703
|
+
content: [
|
|
1704
|
+
{
|
|
1705
|
+
type: "text",
|
|
1706
|
+
text: JSON.stringify(data.modelTemplates, null, 2),
|
|
1707
|
+
},
|
|
1708
|
+
],
|
|
1709
|
+
};
|
|
1710
|
+
});
|
|
1711
|
+
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 }.", {
|
|
1712
|
+
templateId: z
|
|
1713
|
+
.string()
|
|
1714
|
+
.describe("Template id (from list_model_templates)"),
|
|
1715
|
+
}, async ({ templateId }) => {
|
|
1716
|
+
const data = await client.query(INSTALL_MODEL_TEMPLATE_MUTATION, { templateId });
|
|
1717
|
+
return {
|
|
1718
|
+
content: [
|
|
1719
|
+
{
|
|
1720
|
+
type: "text",
|
|
1721
|
+
text: JSON.stringify(data.installModelTemplate, null, 2),
|
|
1722
|
+
},
|
|
1723
|
+
],
|
|
1724
|
+
};
|
|
1725
|
+
});
|
|
1314
1726
|
// ─── Resources ───────────────────────────────────────────────
|
|
1315
1727
|
server.resource("sitemap", "cmssy://sitemap", {
|
|
1316
1728
|
description: "Full page tree as JSON — all pages with hierarchy",
|