multisite-cms-mcp 1.1.1 → 1.2.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/dist/index.js +101 -2
- package/dist/tools/get-field-types.d.ts +30 -0
- package/dist/tools/get-field-types.d.ts.map +1 -0
- package/dist/tools/get-field-types.js +128 -0
- package/dist/tools/sync-schema.d.ts +41 -0
- package/dist/tools/sync-schema.d.ts.map +1 -0
- package/dist/tools/sync-schema.js +409 -0
- package/dist/tools/validate-template.d.ts +6 -1
- package/dist/tools/validate-template.d.ts.map +1 -1
- package/dist/tools/validate-template.js +208 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -14,6 +14,8 @@ const get_tenant_schema_1 = require("./tools/get-tenant-schema");
|
|
|
14
14
|
const list_projects_1 = require("./tools/list-projects");
|
|
15
15
|
const create_site_1 = require("./tools/create-site");
|
|
16
16
|
const deploy_package_1 = require("./tools/deploy-package");
|
|
17
|
+
const get_field_types_1 = require("./tools/get-field-types");
|
|
18
|
+
const sync_schema_1 = require("./tools/sync-schema");
|
|
17
19
|
const server = new index_js_1.Server({
|
|
18
20
|
name: 'multisite-cms',
|
|
19
21
|
version: '1.0.0',
|
|
@@ -49,7 +51,7 @@ const TOOLS = [
|
|
|
49
51
|
},
|
|
50
52
|
{
|
|
51
53
|
name: 'validate_template',
|
|
52
|
-
description: 'Validate an HTML template for correct CMS token usage. Checks for proper {{token}} syntax, correct field names, and proper use of triple braces for rich text.',
|
|
54
|
+
description: 'Validate an HTML template for correct CMS token usage. Checks for proper {{token}} syntax, correct field names, and proper use of triple braces for rich text. When authenticated with a projectId, also validates tokens against the actual project schema and reports missing fields.',
|
|
53
55
|
inputSchema: {
|
|
54
56
|
type: 'object',
|
|
55
57
|
properties: {
|
|
@@ -66,6 +68,10 @@ const TOOLS = [
|
|
|
66
68
|
type: 'string',
|
|
67
69
|
description: 'For custom collections, the collection slug',
|
|
68
70
|
},
|
|
71
|
+
projectId: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Optional: Project ID or name to validate tokens against actual schema. Requires authentication. If missing fields are found, provides instructions for creating them with sync_schema.',
|
|
74
|
+
},
|
|
69
75
|
},
|
|
70
76
|
required: ['html', 'templateType'],
|
|
71
77
|
},
|
|
@@ -206,6 +212,89 @@ const TOOLS = [
|
|
|
206
212
|
required: ['packagePath'],
|
|
207
213
|
},
|
|
208
214
|
},
|
|
215
|
+
{
|
|
216
|
+
name: 'get_field_types',
|
|
217
|
+
description: 'Get the list of available field types for creating new fields in collections. Use this to see what field types you can use with sync_schema. This is NOT the list of fields in your schema - use get_schema or get_tenant_schema for that. No authentication required.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {},
|
|
221
|
+
required: [],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'sync_schema',
|
|
226
|
+
description: 'Create collections and/or add fields to existing collections. Requires authentication. Skips duplicates automatically. All fields must have a type specified - use get_field_types to see available types. Use this after validate_template reports missing fields.',
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {
|
|
230
|
+
projectId: {
|
|
231
|
+
type: 'string',
|
|
232
|
+
description: 'Project ID (UUID) or project name. Use list_projects to see available projects.',
|
|
233
|
+
},
|
|
234
|
+
collections: {
|
|
235
|
+
type: 'array',
|
|
236
|
+
description: 'New collections to create, each with optional fields',
|
|
237
|
+
items: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
slug: { type: 'string', description: 'URL-friendly identifier (lowercase, no spaces)' },
|
|
241
|
+
name: { type: 'string', description: 'Display name (plural)' },
|
|
242
|
+
nameSingular: { type: 'string', description: 'Singular form of name' },
|
|
243
|
+
description: { type: 'string', description: 'Optional description' },
|
|
244
|
+
hasSlug: { type: 'boolean', description: 'Whether items have URL slugs (default: true)' },
|
|
245
|
+
fields: {
|
|
246
|
+
type: 'array',
|
|
247
|
+
description: 'Fields to add to this collection',
|
|
248
|
+
items: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
slug: { type: 'string', description: 'Field identifier' },
|
|
252
|
+
name: { type: 'string', description: 'Field display name' },
|
|
253
|
+
type: { type: 'string', description: 'Field type (REQUIRED) - use get_field_types to see options' },
|
|
254
|
+
description: { type: 'string', description: 'Optional field description' },
|
|
255
|
+
isRequired: { type: 'boolean', description: 'Whether field is required' },
|
|
256
|
+
options: { type: 'string', description: 'For select/multiSelect: comma-separated options' },
|
|
257
|
+
referenceCollection: { type: 'string', description: 'For relation type: slug of referenced collection' },
|
|
258
|
+
},
|
|
259
|
+
required: ['slug', 'name', 'type'],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
required: ['slug', 'name', 'nameSingular'],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
fieldsToAdd: {
|
|
267
|
+
type: 'array',
|
|
268
|
+
description: 'Fields to add to existing collections (builtin or custom)',
|
|
269
|
+
items: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
collectionSlug: { type: 'string', description: 'Collection slug (e.g., "blogs", "team", or custom collection slug)' },
|
|
273
|
+
isBuiltin: { type: 'boolean', description: 'true for blogs/authors/team/downloads, false for custom collections' },
|
|
274
|
+
fields: {
|
|
275
|
+
type: 'array',
|
|
276
|
+
items: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
slug: { type: 'string', description: 'Field identifier' },
|
|
280
|
+
name: { type: 'string', description: 'Field display name' },
|
|
281
|
+
type: { type: 'string', description: 'Field type (REQUIRED) - use get_field_types to see options' },
|
|
282
|
+
description: { type: 'string', description: 'Optional field description' },
|
|
283
|
+
isRequired: { type: 'boolean', description: 'Whether field is required' },
|
|
284
|
+
options: { type: 'string', description: 'For select/multiSelect: comma-separated options' },
|
|
285
|
+
referenceCollection: { type: 'string', description: 'For relation type: slug of referenced collection' },
|
|
286
|
+
},
|
|
287
|
+
required: ['slug', 'name', 'type'],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
required: ['collectionSlug', 'isBuiltin', 'fields'],
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
required: ['projectId'],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
209
298
|
];
|
|
210
299
|
// Handle list tools request
|
|
211
300
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
@@ -225,7 +314,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
225
314
|
result = await (0, validate_manifest_1.validateManifest)(params.manifest);
|
|
226
315
|
break;
|
|
227
316
|
case 'validate_template':
|
|
228
|
-
result = await (0, validate_template_1.validateTemplate)(params.html, params.templateType, params.collectionSlug);
|
|
317
|
+
result = await (0, validate_template_1.validateTemplate)(params.html, params.templateType, params.collectionSlug, params.projectId);
|
|
229
318
|
break;
|
|
230
319
|
case 'validate_package': {
|
|
231
320
|
let templateContents;
|
|
@@ -258,6 +347,16 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
258
347
|
case 'deploy_package':
|
|
259
348
|
result = await (0, deploy_package_1.deployPackage)(params.packagePath, params.projectId, params.projectName);
|
|
260
349
|
break;
|
|
350
|
+
case 'get_field_types':
|
|
351
|
+
result = await (0, get_field_types_1.getFieldTypes)();
|
|
352
|
+
break;
|
|
353
|
+
case 'sync_schema':
|
|
354
|
+
result = await (0, sync_schema_1.syncSchema)({
|
|
355
|
+
projectId: params.projectId,
|
|
356
|
+
collections: params.collections,
|
|
357
|
+
fieldsToAdd: params.fieldsToAdd,
|
|
358
|
+
});
|
|
359
|
+
break;
|
|
261
360
|
default:
|
|
262
361
|
return {
|
|
263
362
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get Available Field Types Tool
|
|
3
|
+
*
|
|
4
|
+
* Returns the list of field types that can be used when CREATING new fields
|
|
5
|
+
* in collections. This is NOT the list of fields in your schema - use
|
|
6
|
+
* get_schema or get_tenant_schema for that.
|
|
7
|
+
*
|
|
8
|
+
* No authentication required.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Field type definition
|
|
12
|
+
*/
|
|
13
|
+
export interface FieldType {
|
|
14
|
+
value: string;
|
|
15
|
+
label: string;
|
|
16
|
+
description: string;
|
|
17
|
+
requiresOptions?: boolean;
|
|
18
|
+
requiresReferenceCollection?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Available field types for creating custom fields
|
|
22
|
+
* This matches the API's supported field types
|
|
23
|
+
*/
|
|
24
|
+
export declare const AVAILABLE_FIELD_TYPES: FieldType[];
|
|
25
|
+
/**
|
|
26
|
+
* Returns the list of available field types for creating fields.
|
|
27
|
+
* No authentication required.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getFieldTypes(): Promise<string>;
|
|
30
|
+
//# sourceMappingURL=get-field-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-field-types.d.ts","sourceRoot":"","sources":["../../src/tools/get-field-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,EAgE5C,CAAC;AAEF;;;GAGG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CA0CrD"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Get Available Field Types Tool
|
|
4
|
+
*
|
|
5
|
+
* Returns the list of field types that can be used when CREATING new fields
|
|
6
|
+
* in collections. This is NOT the list of fields in your schema - use
|
|
7
|
+
* get_schema or get_tenant_schema for that.
|
|
8
|
+
*
|
|
9
|
+
* No authentication required.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AVAILABLE_FIELD_TYPES = void 0;
|
|
13
|
+
exports.getFieldTypes = getFieldTypes;
|
|
14
|
+
/**
|
|
15
|
+
* Available field types for creating custom fields
|
|
16
|
+
* This matches the API's supported field types
|
|
17
|
+
*/
|
|
18
|
+
exports.AVAILABLE_FIELD_TYPES = [
|
|
19
|
+
{
|
|
20
|
+
value: 'text',
|
|
21
|
+
label: 'Text',
|
|
22
|
+
description: 'Single line text field. Use for titles, names, short descriptions.'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: 'richText',
|
|
26
|
+
label: 'Rich Text',
|
|
27
|
+
description: 'Multi-line formatted text (HTML). Use for body content, descriptions with formatting. Use {{{fieldName}}} in templates (triple braces for unescaped HTML).'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
value: 'number',
|
|
31
|
+
label: 'Number',
|
|
32
|
+
description: 'Numeric value. Use for prices, quantities, order numbers.'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: 'boolean',
|
|
36
|
+
label: 'Boolean',
|
|
37
|
+
description: 'True/False toggle. Use for featured flags, visibility toggles.'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: 'date',
|
|
41
|
+
label: 'Date',
|
|
42
|
+
description: 'Date picker (date only). Use for publication dates, event dates.'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: 'datetime',
|
|
46
|
+
label: 'Date & Time',
|
|
47
|
+
description: 'Date and time picker. Use when you need both date and time.'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: 'image',
|
|
51
|
+
label: 'Image URL',
|
|
52
|
+
description: 'URL to an image. Use for photos, thumbnails, hero images.'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
value: 'url',
|
|
56
|
+
label: 'URL',
|
|
57
|
+
description: 'Web link. Use for external links, social media URLs.'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: 'email',
|
|
61
|
+
label: 'Email',
|
|
62
|
+
description: 'Email address field with validation.'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
value: 'select',
|
|
66
|
+
label: 'Select',
|
|
67
|
+
description: 'Dropdown with predefined options. Requires "options" parameter with comma-separated values.',
|
|
68
|
+
requiresOptions: true,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
value: 'multiSelect',
|
|
72
|
+
label: 'Multi-Select',
|
|
73
|
+
description: 'Multiple selections from options. Requires "options" parameter with comma-separated values.',
|
|
74
|
+
requiresOptions: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
value: 'relation',
|
|
78
|
+
label: 'Relation',
|
|
79
|
+
description: 'Link to another collection item. Requires "referenceCollection" parameter with the collection slug. Only for custom collections.',
|
|
80
|
+
requiresReferenceCollection: true,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* Returns the list of available field types for creating fields.
|
|
85
|
+
* No authentication required.
|
|
86
|
+
*/
|
|
87
|
+
async function getFieldTypes() {
|
|
88
|
+
const typeList = exports.AVAILABLE_FIELD_TYPES.map(ft => {
|
|
89
|
+
let entry = `- **${ft.value}** (${ft.label}): ${ft.description}`;
|
|
90
|
+
if (ft.requiresOptions) {
|
|
91
|
+
entry += '\n - ⚠️ Requires `options` parameter (comma-separated values)';
|
|
92
|
+
}
|
|
93
|
+
if (ft.requiresReferenceCollection) {
|
|
94
|
+
entry += '\n - ⚠️ Requires `referenceCollection` parameter (collection slug)';
|
|
95
|
+
}
|
|
96
|
+
return entry;
|
|
97
|
+
}).join('\n');
|
|
98
|
+
return `# Available Field Types
|
|
99
|
+
|
|
100
|
+
These are the field types you can use when CREATING new fields with the \`sync_schema\` tool.
|
|
101
|
+
|
|
102
|
+
**Note:** This is NOT the list of fields in your schema. Use \`get_schema\` for the generic schema or \`get_tenant_schema\` for a specific project's schema.
|
|
103
|
+
|
|
104
|
+
## Field Types
|
|
105
|
+
|
|
106
|
+
${typeList}
|
|
107
|
+
|
|
108
|
+
## Usage Example
|
|
109
|
+
|
|
110
|
+
When calling \`sync_schema\` to add fields, specify the \`type\` parameter:
|
|
111
|
+
|
|
112
|
+
\`\`\`json
|
|
113
|
+
{
|
|
114
|
+
"fieldsToAdd": [
|
|
115
|
+
{
|
|
116
|
+
"collectionSlug": "blogs",
|
|
117
|
+
"isBuiltin": true,
|
|
118
|
+
"fields": [
|
|
119
|
+
{ "slug": "heroImage", "name": "Hero Image", "type": "image" },
|
|
120
|
+
{ "slug": "featured", "name": "Featured", "type": "boolean" },
|
|
121
|
+
{ "slug": "category", "name": "Category", "type": "select", "options": "Tech,Business,Lifestyle" }
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
\`\`\`
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Schema Tool
|
|
3
|
+
*
|
|
4
|
+
* Creates collections and/or fields in Fast Mode.
|
|
5
|
+
* Requires authentication. Will skip duplicates.
|
|
6
|
+
*/
|
|
7
|
+
interface FieldToCreate {
|
|
8
|
+
slug: string;
|
|
9
|
+
name: string;
|
|
10
|
+
type: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
isRequired?: boolean;
|
|
13
|
+
options?: string;
|
|
14
|
+
referenceCollection?: string;
|
|
15
|
+
}
|
|
16
|
+
interface CollectionToCreate {
|
|
17
|
+
slug: string;
|
|
18
|
+
name: string;
|
|
19
|
+
nameSingular: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
hasSlug?: boolean;
|
|
22
|
+
fields?: FieldToCreate[];
|
|
23
|
+
}
|
|
24
|
+
interface FieldsToAdd {
|
|
25
|
+
collectionSlug: string;
|
|
26
|
+
isBuiltin: boolean;
|
|
27
|
+
fields: FieldToCreate[];
|
|
28
|
+
}
|
|
29
|
+
interface SyncSchemaInput {
|
|
30
|
+
projectId: string;
|
|
31
|
+
collections?: CollectionToCreate[];
|
|
32
|
+
fieldsToAdd?: FieldsToAdd[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Sync schema - create collections and/or fields
|
|
36
|
+
*
|
|
37
|
+
* @param input - The sync schema input with projectId, collections, and/or fieldsToAdd
|
|
38
|
+
*/
|
|
39
|
+
export declare function syncSchema(input: SyncSchemaInput): Promise<string>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=sync-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-schema.d.ts","sourceRoot":"","sources":["../../src/tools/sync-schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,UAAU,WAAW;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,UAAU,eAAe;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACnC,WAAW,CAAC,EAAE,WAAW,EAAE,CAAC;CAC7B;AAyKD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA8UxE"}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sync Schema Tool
|
|
4
|
+
*
|
|
5
|
+
* Creates collections and/or fields in Fast Mode.
|
|
6
|
+
* Requires authentication. Will skip duplicates.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.syncSchema = syncSchema;
|
|
10
|
+
const api_client_1 = require("../lib/api-client");
|
|
11
|
+
const device_flow_1 = require("../lib/device-flow");
|
|
12
|
+
const get_field_types_1 = require("./get-field-types");
|
|
13
|
+
// ============ Constants ============
|
|
14
|
+
const BUILTIN_COLLECTION_TYPES = ['blogs', 'authors', 'team', 'downloads'];
|
|
15
|
+
const VALID_FIELD_TYPES = get_field_types_1.AVAILABLE_FIELD_TYPES.map(ft => ft.value);
|
|
16
|
+
const AUTH_REQUIRED_MESSAGE = `# Authentication Required
|
|
17
|
+
|
|
18
|
+
This tool requires authentication to create collections and fields.
|
|
19
|
+
|
|
20
|
+
**To authenticate:**
|
|
21
|
+
1. Set the FASTMODE_AUTH_TOKEN environment variable, OR
|
|
22
|
+
2. Run this tool again and follow the browser-based login flow
|
|
23
|
+
|
|
24
|
+
Use \`list_projects\` to verify your authentication status.
|
|
25
|
+
`;
|
|
26
|
+
// ============ Helper Functions ============
|
|
27
|
+
/**
|
|
28
|
+
* Validate a field type against available types
|
|
29
|
+
*/
|
|
30
|
+
function validateFieldType(type) {
|
|
31
|
+
if (!type) {
|
|
32
|
+
return { valid: false, error: 'Field type is required' };
|
|
33
|
+
}
|
|
34
|
+
if (!VALID_FIELD_TYPES.includes(type)) {
|
|
35
|
+
return {
|
|
36
|
+
valid: false,
|
|
37
|
+
error: `Invalid field type "${type}". Valid types: ${VALID_FIELD_TYPES.join(', ')}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { valid: true };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate all fields in input
|
|
44
|
+
*/
|
|
45
|
+
function validateFields(fields) {
|
|
46
|
+
const errors = [];
|
|
47
|
+
for (const field of fields) {
|
|
48
|
+
if (!field.slug) {
|
|
49
|
+
errors.push(`Field missing slug`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!field.name) {
|
|
53
|
+
errors.push(`Field "${field.slug}" missing name`);
|
|
54
|
+
}
|
|
55
|
+
if (!field.type) {
|
|
56
|
+
errors.push(`Field "${field.slug}" missing type. Use get_field_types to see available types.`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const typeValidation = validateFieldType(field.type);
|
|
60
|
+
if (!typeValidation.valid) {
|
|
61
|
+
errors.push(`Field "${field.slug}": ${typeValidation.error}`);
|
|
62
|
+
}
|
|
63
|
+
// Check for required options/referenceCollection
|
|
64
|
+
if ((field.type === 'select' || field.type === 'multiSelect') && !field.options) {
|
|
65
|
+
errors.push(`Field "${field.slug}" (${field.type}) requires "options" parameter with comma-separated values`);
|
|
66
|
+
}
|
|
67
|
+
if (field.type === 'relation' && !field.referenceCollection) {
|
|
68
|
+
errors.push(`Field "${field.slug}" (relation) requires "referenceCollection" parameter`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { valid: errors.length === 0, errors };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve project identifier to tenant ID
|
|
75
|
+
*/
|
|
76
|
+
async function resolveProjectId(projectIdentifier) {
|
|
77
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
78
|
+
if (uuidPattern.test(projectIdentifier)) {
|
|
79
|
+
return { tenantId: projectIdentifier };
|
|
80
|
+
}
|
|
81
|
+
const response = await (0, api_client_1.apiRequest)('/api/tenants');
|
|
82
|
+
if ((0, api_client_1.isApiError)(response)) {
|
|
83
|
+
return { error: `Failed to look up project: ${response.error}` };
|
|
84
|
+
}
|
|
85
|
+
const match = response.data.find(p => p.name.toLowerCase() === projectIdentifier.toLowerCase());
|
|
86
|
+
if (match) {
|
|
87
|
+
return { tenantId: match.id };
|
|
88
|
+
}
|
|
89
|
+
const partialMatch = response.data.find(p => p.name.toLowerCase().includes(projectIdentifier.toLowerCase()));
|
|
90
|
+
if (partialMatch) {
|
|
91
|
+
return { tenantId: partialMatch.id };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
error: `Project "${projectIdentifier}" not found. Use list_projects to see available projects.`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Fetch existing schema for a project
|
|
99
|
+
*/
|
|
100
|
+
async function fetchExistingSchema(tenantId) {
|
|
101
|
+
// Fetch custom collections
|
|
102
|
+
const collectionsRes = await (0, api_client_1.apiRequest)('/api/collections', { tenantId });
|
|
103
|
+
if ((0, api_client_1.isApiError)(collectionsRes)) {
|
|
104
|
+
return { error: `Failed to fetch collections: ${collectionsRes.error}` };
|
|
105
|
+
}
|
|
106
|
+
// Fetch builtin collection fields
|
|
107
|
+
const builtinRes = await (0, api_client_1.apiRequest)('/api/cms/builtin-collections', { tenantId });
|
|
108
|
+
const builtinFields = {};
|
|
109
|
+
if (!(0, api_client_1.isApiError)(builtinRes)) {
|
|
110
|
+
for (const bc of builtinRes.data) {
|
|
111
|
+
builtinFields[bc.type] = bc.fields.map(f => ({ slug: f.slug, name: f.name, type: f.type }));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
collections: collectionsRes.data,
|
|
116
|
+
builtinFields,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ============ Main Function ============
|
|
120
|
+
/**
|
|
121
|
+
* Sync schema - create collections and/or fields
|
|
122
|
+
*
|
|
123
|
+
* @param input - The sync schema input with projectId, collections, and/or fieldsToAdd
|
|
124
|
+
*/
|
|
125
|
+
async function syncSchema(input) {
|
|
126
|
+
// Check authentication
|
|
127
|
+
if (await (0, api_client_1.needsAuthentication)()) {
|
|
128
|
+
const authResult = await (0, device_flow_1.ensureAuthenticated)();
|
|
129
|
+
if (!authResult.authenticated) {
|
|
130
|
+
return AUTH_REQUIRED_MESSAGE;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const { projectId, collections, fieldsToAdd } = input;
|
|
134
|
+
// Validate input
|
|
135
|
+
if (!projectId) {
|
|
136
|
+
return `# Error: Missing projectId
|
|
137
|
+
|
|
138
|
+
Please provide a projectId. Use \`list_projects\` to see your available projects.
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
if ((!collections || collections.length === 0) && (!fieldsToAdd || fieldsToAdd.length === 0)) {
|
|
142
|
+
return `# Error: Nothing to sync
|
|
143
|
+
|
|
144
|
+
Please provide either:
|
|
145
|
+
- \`collections\`: Array of new collections to create
|
|
146
|
+
- \`fieldsToAdd\`: Array of fields to add to existing collections
|
|
147
|
+
|
|
148
|
+
Use \`get_field_types\` to see available field types.
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
// Validate all field types before making any API calls
|
|
152
|
+
const allValidationErrors = [];
|
|
153
|
+
if (collections) {
|
|
154
|
+
for (const col of collections) {
|
|
155
|
+
if (!col.slug)
|
|
156
|
+
allValidationErrors.push(`Collection missing slug`);
|
|
157
|
+
if (!col.name)
|
|
158
|
+
allValidationErrors.push(`Collection "${col.slug || 'unknown'}" missing name`);
|
|
159
|
+
if (!col.nameSingular)
|
|
160
|
+
allValidationErrors.push(`Collection "${col.slug || 'unknown'}" missing nameSingular`);
|
|
161
|
+
if (col.fields && col.fields.length > 0) {
|
|
162
|
+
const fieldValidation = validateFields(col.fields);
|
|
163
|
+
if (!fieldValidation.valid) {
|
|
164
|
+
allValidationErrors.push(...fieldValidation.errors.map(e => `Collection "${col.slug}": ${e}`));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (fieldsToAdd) {
|
|
170
|
+
for (const group of fieldsToAdd) {
|
|
171
|
+
if (!group.collectionSlug) {
|
|
172
|
+
allValidationErrors.push(`fieldsToAdd entry missing collectionSlug`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (group.isBuiltin && !BUILTIN_COLLECTION_TYPES.includes(group.collectionSlug)) {
|
|
176
|
+
allValidationErrors.push(`"${group.collectionSlug}" is not a valid builtin collection. Valid: ${BUILTIN_COLLECTION_TYPES.join(', ')}`);
|
|
177
|
+
}
|
|
178
|
+
if (!group.fields || group.fields.length === 0) {
|
|
179
|
+
allValidationErrors.push(`fieldsToAdd for "${group.collectionSlug}" has no fields`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const fieldValidation = validateFields(group.fields);
|
|
183
|
+
if (!fieldValidation.valid) {
|
|
184
|
+
allValidationErrors.push(...fieldValidation.errors.map(e => `${group.collectionSlug}: ${e}`));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (allValidationErrors.length > 0) {
|
|
189
|
+
return `# Validation Errors
|
|
190
|
+
|
|
191
|
+
Please fix the following errors before syncing:
|
|
192
|
+
|
|
193
|
+
${allValidationErrors.map(e => `- ${e}`).join('\n')}
|
|
194
|
+
|
|
195
|
+
**Tip:** Use \`get_field_types\` to see available field types and their requirements.
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
// Resolve project ID
|
|
199
|
+
const resolved = await resolveProjectId(projectId);
|
|
200
|
+
if ('error' in resolved) {
|
|
201
|
+
return `# Project Not Found
|
|
202
|
+
|
|
203
|
+
${resolved.error}
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
const { tenantId } = resolved;
|
|
207
|
+
// Fetch existing schema
|
|
208
|
+
const existingSchema = await fetchExistingSchema(tenantId);
|
|
209
|
+
if ('error' in existingSchema) {
|
|
210
|
+
// Check if auth error
|
|
211
|
+
if (existingSchema.error.includes('401') || existingSchema.error.includes('auth')) {
|
|
212
|
+
const authResult = await (0, device_flow_1.ensureAuthenticated)();
|
|
213
|
+
if (!authResult.authenticated) {
|
|
214
|
+
return AUTH_REQUIRED_MESSAGE;
|
|
215
|
+
}
|
|
216
|
+
// Retry
|
|
217
|
+
const retry = await fetchExistingSchema(tenantId);
|
|
218
|
+
if ('error' in retry) {
|
|
219
|
+
return `# Error\n\n${retry.error}`;
|
|
220
|
+
}
|
|
221
|
+
Object.assign(existingSchema, retry);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
return `# Error\n\n${existingSchema.error}`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const { collections: existingCollections, builtinFields } = existingSchema;
|
|
228
|
+
// Track results
|
|
229
|
+
const results = [];
|
|
230
|
+
const created = { collections: 0, fields: 0 };
|
|
231
|
+
const skipped = { collections: 0, fields: 0 };
|
|
232
|
+
// Create new collections
|
|
233
|
+
if (collections && collections.length > 0) {
|
|
234
|
+
for (const col of collections) {
|
|
235
|
+
// Check if collection already exists
|
|
236
|
+
const existing = existingCollections.find(c => c.slug.toLowerCase() === col.slug.toLowerCase());
|
|
237
|
+
if (existing) {
|
|
238
|
+
results.push(`⏭️ Skipped collection "${col.slug}" (already exists)`);
|
|
239
|
+
skipped.collections++;
|
|
240
|
+
// But still try to add any new fields
|
|
241
|
+
if (col.fields && col.fields.length > 0) {
|
|
242
|
+
for (const field of col.fields) {
|
|
243
|
+
const fieldExists = existing.fields.some(f => f.slug.toLowerCase() === field.slug.toLowerCase());
|
|
244
|
+
if (fieldExists) {
|
|
245
|
+
results.push(` ⏭️ Skipped field "${field.slug}" (already exists)`);
|
|
246
|
+
skipped.fields++;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// Add field to existing collection
|
|
250
|
+
const fieldRes = await (0, api_client_1.apiRequest)(`/api/collections/${existing.id}/fields`, {
|
|
251
|
+
tenantId,
|
|
252
|
+
method: 'POST',
|
|
253
|
+
body: {
|
|
254
|
+
slug: field.slug,
|
|
255
|
+
name: field.name,
|
|
256
|
+
type: field.type,
|
|
257
|
+
description: field.description,
|
|
258
|
+
isRequired: field.isRequired,
|
|
259
|
+
options: field.options,
|
|
260
|
+
referenceCollection: field.referenceCollection,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
if ((0, api_client_1.isApiError)(fieldRes)) {
|
|
264
|
+
results.push(` ❌ Failed to add field "${field.slug}": ${fieldRes.error}`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
results.push(` ✅ Added field "${field.slug}" (${field.type})`);
|
|
268
|
+
created.fields++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
// Create new collection
|
|
276
|
+
const createRes = await (0, api_client_1.apiRequest)('/api/collections', {
|
|
277
|
+
tenantId,
|
|
278
|
+
method: 'POST',
|
|
279
|
+
body: {
|
|
280
|
+
slug: col.slug,
|
|
281
|
+
name: col.name,
|
|
282
|
+
nameSingular: col.nameSingular,
|
|
283
|
+
description: col.description,
|
|
284
|
+
hasSlug: col.hasSlug ?? true,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
if ((0, api_client_1.isApiError)(createRes)) {
|
|
288
|
+
results.push(`❌ Failed to create collection "${col.slug}": ${createRes.error}`);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
results.push(`✅ Created collection "${col.name}" (${col.slug})`);
|
|
292
|
+
created.collections++;
|
|
293
|
+
// Add fields to the new collection
|
|
294
|
+
if (col.fields && col.fields.length > 0) {
|
|
295
|
+
const collectionId = createRes.data.id;
|
|
296
|
+
for (const field of col.fields) {
|
|
297
|
+
const fieldRes = await (0, api_client_1.apiRequest)(`/api/collections/${collectionId}/fields`, {
|
|
298
|
+
tenantId,
|
|
299
|
+
method: 'POST',
|
|
300
|
+
body: {
|
|
301
|
+
slug: field.slug,
|
|
302
|
+
name: field.name,
|
|
303
|
+
type: field.type,
|
|
304
|
+
description: field.description,
|
|
305
|
+
isRequired: field.isRequired,
|
|
306
|
+
options: field.options,
|
|
307
|
+
referenceCollection: field.referenceCollection,
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
if ((0, api_client_1.isApiError)(fieldRes)) {
|
|
311
|
+
results.push(` ❌ Failed to add field "${field.slug}": ${fieldRes.error}`);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
results.push(` ✅ Added field "${field.slug}" (${field.type})`);
|
|
315
|
+
created.fields++;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Add fields to existing collections
|
|
322
|
+
if (fieldsToAdd && fieldsToAdd.length > 0) {
|
|
323
|
+
for (const group of fieldsToAdd) {
|
|
324
|
+
results.push(`\n📁 ${group.collectionSlug} (${group.isBuiltin ? 'builtin' : 'custom'}):`);
|
|
325
|
+
if (group.isBuiltin) {
|
|
326
|
+
// Add to builtin collection
|
|
327
|
+
const existingBuiltinFields = builtinFields[group.collectionSlug] || [];
|
|
328
|
+
for (const field of group.fields) {
|
|
329
|
+
const fieldExists = existingBuiltinFields.some(f => f.slug.toLowerCase() === field.slug.toLowerCase());
|
|
330
|
+
if (fieldExists) {
|
|
331
|
+
results.push(` ⏭️ Skipped field "${field.slug}" (already exists)`);
|
|
332
|
+
skipped.fields++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const fieldRes = await (0, api_client_1.apiRequest)(`/api/cms/builtin-collections/${group.collectionSlug}/fields`, {
|
|
336
|
+
tenantId,
|
|
337
|
+
method: 'POST',
|
|
338
|
+
body: {
|
|
339
|
+
slug: field.slug,
|
|
340
|
+
name: field.name,
|
|
341
|
+
type: field.type,
|
|
342
|
+
description: field.description,
|
|
343
|
+
isRequired: field.isRequired,
|
|
344
|
+
options: field.options,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
if ((0, api_client_1.isApiError)(fieldRes)) {
|
|
348
|
+
results.push(` ❌ Failed to add field "${field.slug}": ${fieldRes.error}`);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
results.push(` ✅ Added field "${field.slug}" (${field.type})`);
|
|
352
|
+
created.fields++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// Add to custom collection
|
|
358
|
+
const customCollection = existingCollections.find(c => c.slug.toLowerCase() === group.collectionSlug.toLowerCase());
|
|
359
|
+
if (!customCollection) {
|
|
360
|
+
results.push(` ❌ Collection "${group.collectionSlug}" not found. Create it first with the collections parameter.`);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
for (const field of group.fields) {
|
|
364
|
+
const fieldExists = customCollection.fields.some(f => f.slug.toLowerCase() === field.slug.toLowerCase());
|
|
365
|
+
if (fieldExists) {
|
|
366
|
+
results.push(` ⏭️ Skipped field "${field.slug}" (already exists)`);
|
|
367
|
+
skipped.fields++;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const fieldRes = await (0, api_client_1.apiRequest)(`/api/collections/${customCollection.id}/fields`, {
|
|
371
|
+
tenantId,
|
|
372
|
+
method: 'POST',
|
|
373
|
+
body: {
|
|
374
|
+
slug: field.slug,
|
|
375
|
+
name: field.name,
|
|
376
|
+
type: field.type,
|
|
377
|
+
description: field.description,
|
|
378
|
+
isRequired: field.isRequired,
|
|
379
|
+
options: field.options,
|
|
380
|
+
referenceCollection: field.referenceCollection,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
if ((0, api_client_1.isApiError)(fieldRes)) {
|
|
384
|
+
results.push(` ❌ Failed to add field "${field.slug}": ${fieldRes.error}`);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
results.push(` ✅ Added field "${field.slug}" (${field.type})`);
|
|
388
|
+
created.fields++;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Summary
|
|
395
|
+
return `# Schema Sync Complete
|
|
396
|
+
|
|
397
|
+
**Project ID:** \`${tenantId}\`
|
|
398
|
+
|
|
399
|
+
## Summary
|
|
400
|
+
- Collections created: ${created.collections}
|
|
401
|
+
- Fields created: ${created.fields}
|
|
402
|
+
- Collections skipped (already exist): ${skipped.collections}
|
|
403
|
+
- Fields skipped (already exist): ${skipped.fields}
|
|
404
|
+
|
|
405
|
+
## Details
|
|
406
|
+
|
|
407
|
+
${results.join('\n')}
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
type TemplateType = 'blog_index' | 'blog_post' | 'team' | 'downloads' | 'authors_index' | 'author_detail' | 'custom_index' | 'custom_detail' | 'static_page';
|
|
2
2
|
/**
|
|
3
3
|
* Validates an HTML template for correct CMS token usage
|
|
4
|
+
*
|
|
5
|
+
* @param html - The HTML template content
|
|
6
|
+
* @param templateType - The type of template
|
|
7
|
+
* @param collectionSlug - For custom collections, the collection slug
|
|
8
|
+
* @param projectId - Optional: Project ID or name to validate against actual schema
|
|
4
9
|
*/
|
|
5
|
-
export declare function validateTemplate(html: string, templateType: TemplateType, collectionSlug?: string): Promise<string>;
|
|
10
|
+
export declare function validateTemplate(html: string, templateType: TemplateType, collectionSlug?: string, projectId?: string): Promise<string>;
|
|
6
11
|
export {};
|
|
7
12
|
//# sourceMappingURL=validate-template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-template.d.ts","sourceRoot":"","sources":["../../src/tools/validate-template.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate-template.d.ts","sourceRoot":"","sources":["../../src/tools/validate-template.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG,YAAY,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,eAAe,GAAG,eAAe,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;AA6O7J;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,YAAY,EAC1B,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAwVjB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateTemplate = validateTemplate;
|
|
4
|
+
const api_client_1 = require("../lib/api-client");
|
|
4
5
|
const BLOG_FIELDS = [
|
|
5
6
|
{ name: 'name', isRichText: false, isRequired: true },
|
|
6
7
|
{ name: 'slug', isRichText: false, isRequired: true },
|
|
@@ -62,10 +63,118 @@ const COMMON_MISTAKES = {
|
|
|
62
63
|
'position': 'role',
|
|
63
64
|
'jobTitle': 'role',
|
|
64
65
|
};
|
|
66
|
+
// Built-in collection standard fields (not including custom fields)
|
|
67
|
+
const BUILTIN_STANDARD_FIELDS = {
|
|
68
|
+
blogs: ['name', 'slug', 'mainImage', 'thumbnailImage', 'postSummary', 'postBody', 'featured', 'publishedAt', 'author', 'url'],
|
|
69
|
+
authors: ['name', 'slug', 'bio', 'picture', 'email', 'twitterProfileLink', 'linkedinProfileLink', 'url'],
|
|
70
|
+
team: ['name', 'slug', 'role', 'bio', 'photo', 'email', 'order'],
|
|
71
|
+
downloads: ['title', 'slug', 'description', 'fileUrl', 'category', 'order'],
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Resolve project identifier to tenant ID
|
|
75
|
+
*/
|
|
76
|
+
async function resolveProjectId(projectIdentifier) {
|
|
77
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
78
|
+
if (uuidPattern.test(projectIdentifier)) {
|
|
79
|
+
return { tenantId: projectIdentifier };
|
|
80
|
+
}
|
|
81
|
+
const response = await (0, api_client_1.apiRequest)('/api/tenants');
|
|
82
|
+
if ((0, api_client_1.isApiError)(response)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const match = response.data.find(p => p.name.toLowerCase() === projectIdentifier.toLowerCase() ||
|
|
86
|
+
p.name.toLowerCase().includes(projectIdentifier.toLowerCase()));
|
|
87
|
+
return match ? { tenantId: match.id } : null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Fetch tenant schema for validation
|
|
91
|
+
*/
|
|
92
|
+
async function fetchTenantSchema(tenantId) {
|
|
93
|
+
// Fetch custom collections
|
|
94
|
+
const collectionsRes = await (0, api_client_1.apiRequest)('/api/collections', { tenantId });
|
|
95
|
+
if ((0, api_client_1.isApiError)(collectionsRes)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
// Fetch builtin collection fields
|
|
99
|
+
const builtinRes = await (0, api_client_1.apiRequest)('/api/cms/builtin-collections', { tenantId });
|
|
100
|
+
const builtinFields = {};
|
|
101
|
+
if (!(0, api_client_1.isApiError)(builtinRes)) {
|
|
102
|
+
for (const bc of builtinRes.data) {
|
|
103
|
+
builtinFields[bc.type] = bc.fields;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
collections: collectionsRes.data,
|
|
108
|
+
builtinFields,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Extract field names used in template tokens
|
|
113
|
+
*/
|
|
114
|
+
function extractTokenFieldNames(html) {
|
|
115
|
+
const fields = new Set();
|
|
116
|
+
// Match {{fieldName}} and {{{fieldName}}}
|
|
117
|
+
const doubleTokens = html.match(/\{\{([^{}#/][^{}]*)\}\}/g) || [];
|
|
118
|
+
const tripleTokens = html.match(/\{\{\{([^{}]+)\}\}\}/g) || [];
|
|
119
|
+
for (const token of [...doubleTokens, ...tripleTokens]) {
|
|
120
|
+
const fieldName = token.replace(/\{|\}/g, '').trim();
|
|
121
|
+
// Skip special tokens
|
|
122
|
+
if (fieldName.startsWith('#') || fieldName.startsWith('/') ||
|
|
123
|
+
fieldName === 'else' || fieldName.startsWith('@') ||
|
|
124
|
+
fieldName.startsWith('this') || fieldName.startsWith('../')) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Get the base field name (before any dots)
|
|
128
|
+
const baseName = fieldName.split('.')[0];
|
|
129
|
+
if (baseName && baseName !== 'site') {
|
|
130
|
+
fields.add(baseName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return Array.from(fields);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check which fields are missing from schema
|
|
137
|
+
*/
|
|
138
|
+
function findMissingFields(usedFields, collectionType, isBuiltin, schema) {
|
|
139
|
+
const missing = [];
|
|
140
|
+
if (isBuiltin) {
|
|
141
|
+
// Check against builtin standard fields + custom fields
|
|
142
|
+
const standardFields = BUILTIN_STANDARD_FIELDS[collectionType] || [];
|
|
143
|
+
const customFields = schema.builtinFields[collectionType]?.map(f => f.slug) || [];
|
|
144
|
+
const allFields = [...standardFields, ...customFields];
|
|
145
|
+
for (const field of usedFields) {
|
|
146
|
+
if (!allFields.includes(field)) {
|
|
147
|
+
missing.push(field);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Check against custom collection fields
|
|
153
|
+
const collection = schema.collections.find(c => c.slug === collectionType);
|
|
154
|
+
if (collection) {
|
|
155
|
+
const collectionFields = ['name', 'slug', 'publishedAt', ...collection.fields.map(f => f.slug)];
|
|
156
|
+
for (const field of usedFields) {
|
|
157
|
+
if (!collectionFields.includes(field)) {
|
|
158
|
+
missing.push(field);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Collection doesn't exist - all fields are "missing"
|
|
164
|
+
return usedFields;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return missing;
|
|
168
|
+
}
|
|
65
169
|
/**
|
|
66
170
|
* Validates an HTML template for correct CMS token usage
|
|
171
|
+
*
|
|
172
|
+
* @param html - The HTML template content
|
|
173
|
+
* @param templateType - The type of template
|
|
174
|
+
* @param collectionSlug - For custom collections, the collection slug
|
|
175
|
+
* @param projectId - Optional: Project ID or name to validate against actual schema
|
|
67
176
|
*/
|
|
68
|
-
async function validateTemplate(html, templateType, collectionSlug) {
|
|
177
|
+
async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
69
178
|
const errors = [];
|
|
70
179
|
const warnings = [];
|
|
71
180
|
const suggestions = [];
|
|
@@ -236,6 +345,100 @@ async function validateTemplate(html, templateType, collectionSlug) {
|
|
|
236
345
|
if (eqBlocks.length !== eqCloses) {
|
|
237
346
|
errors.push(`- Unbalanced {{#eq}}: ${eqBlocks.length} opens, ${eqCloses} closes`);
|
|
238
347
|
}
|
|
348
|
+
// ============ Schema Validation (if authenticated with projectId) ============
|
|
349
|
+
let schemaValidation = '';
|
|
350
|
+
if (projectId && !(await (0, api_client_1.needsAuthentication)())) {
|
|
351
|
+
// Resolve project
|
|
352
|
+
const resolved = await resolveProjectId(projectId);
|
|
353
|
+
if (resolved) {
|
|
354
|
+
const schema = await fetchTenantSchema(resolved.tenantId);
|
|
355
|
+
if (schema) {
|
|
356
|
+
// Determine which collection we're validating
|
|
357
|
+
let targetCollection = collectionSlug || '';
|
|
358
|
+
let isBuiltin = false;
|
|
359
|
+
switch (templateType) {
|
|
360
|
+
case 'blog_index':
|
|
361
|
+
case 'blog_post':
|
|
362
|
+
targetCollection = 'blogs';
|
|
363
|
+
isBuiltin = true;
|
|
364
|
+
break;
|
|
365
|
+
case 'authors_index':
|
|
366
|
+
case 'author_detail':
|
|
367
|
+
targetCollection = 'authors';
|
|
368
|
+
isBuiltin = true;
|
|
369
|
+
break;
|
|
370
|
+
case 'team':
|
|
371
|
+
targetCollection = 'team';
|
|
372
|
+
isBuiltin = true;
|
|
373
|
+
break;
|
|
374
|
+
case 'downloads':
|
|
375
|
+
targetCollection = 'downloads';
|
|
376
|
+
isBuiltin = true;
|
|
377
|
+
break;
|
|
378
|
+
case 'custom_index':
|
|
379
|
+
case 'custom_detail':
|
|
380
|
+
isBuiltin = false;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
if (targetCollection && templateType !== 'static_page') {
|
|
384
|
+
// Extract fields used in template
|
|
385
|
+
const usedFields = extractTokenFieldNames(html);
|
|
386
|
+
// Find missing fields
|
|
387
|
+
const missingFields = findMissingFields(usedFields, targetCollection, isBuiltin, schema);
|
|
388
|
+
if (missingFields.length > 0) {
|
|
389
|
+
schemaValidation = `
|
|
390
|
+
|
|
391
|
+
## ⚠️ Missing Fields in Schema
|
|
392
|
+
|
|
393
|
+
The following fields are used in the template but don't exist in the "${targetCollection}" collection:
|
|
394
|
+
|
|
395
|
+
${missingFields.map(f => `- \`${f}\``).join('\n')}
|
|
396
|
+
|
|
397
|
+
**To create these fields, use the \`sync_schema\` tool:**
|
|
398
|
+
|
|
399
|
+
\`\`\`json
|
|
400
|
+
{
|
|
401
|
+
"projectId": "${resolved.tenantId}",
|
|
402
|
+
"fieldsToAdd": [
|
|
403
|
+
{
|
|
404
|
+
"collectionSlug": "${targetCollection}",
|
|
405
|
+
"isBuiltin": ${isBuiltin},
|
|
406
|
+
"fields": [
|
|
407
|
+
${missingFields.map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1')}", "type": "YOUR_TYPE_HERE" }`).join(',\n')}
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
]
|
|
411
|
+
}
|
|
412
|
+
\`\`\`
|
|
413
|
+
|
|
414
|
+
**Important:** Replace \`"type": "YOUR_TYPE_HERE"\` with the appropriate field type.
|
|
415
|
+
Use \`get_field_types\` to see available field types.
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
schemaValidation = `
|
|
420
|
+
|
|
421
|
+
## ✅ Schema Validation Passed
|
|
422
|
+
|
|
423
|
+
All fields used in this template exist in the "${targetCollection}" collection.
|
|
424
|
+
`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else if (projectId) {
|
|
431
|
+
// projectId provided but not authenticated
|
|
432
|
+
schemaValidation = `
|
|
433
|
+
|
|
434
|
+
## ℹ️ Schema Validation Skipped
|
|
435
|
+
|
|
436
|
+
A projectId was provided but you're not authenticated.
|
|
437
|
+
To validate against your project's schema, authenticate first using the device flow or FASTMODE_AUTH_TOKEN.
|
|
438
|
+
|
|
439
|
+
Without authentication, only syntax validation is performed.
|
|
440
|
+
`;
|
|
441
|
+
}
|
|
239
442
|
// Build result
|
|
240
443
|
let output = '';
|
|
241
444
|
if (errors.length === 0 && warnings.length === 0) {
|
|
@@ -279,5 +482,9 @@ Warnings:
|
|
|
279
482
|
${warnings.join('\n')}`;
|
|
280
483
|
}
|
|
281
484
|
}
|
|
485
|
+
// Add schema validation results if available
|
|
486
|
+
if (schemaValidation) {
|
|
487
|
+
output += schemaValidation;
|
|
488
|
+
}
|
|
282
489
|
return output;
|
|
283
490
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multisite-cms-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for Fast Mode CMS. Convert websites, validate packages, and deploy directly to Fast Mode. Includes authentication, project creation, and one-click deployment.",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "MCP server for Fast Mode CMS. Convert websites, validate packages, and deploy directly to Fast Mode. Includes authentication, project creation, schema sync, and one-click deployment.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"multisite-cms-mcp": "./dist/index.js"
|