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 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":"AAAA,KAAK,YAAY,GAAG,YAAY,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,eAAe,GAAG,eAAe,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;AA0E7J;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,YAAY,EAC1B,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,MAAM,CAAC,CA+OjB"}
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.1.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"