multisite-cms-mcp 1.5.7 → 1.5.9
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 +27 -0
- package/dist/tools/generate-samples.d.ts +19 -0
- package/dist/tools/generate-samples.d.ts.map +1 -0
- package/dist/tools/generate-samples.js +272 -0
- package/dist/tools/get-conversion-guide.d.ts.map +1 -1
- package/dist/tools/get-conversion-guide.js +28 -3
- package/dist/tools/get-example.d.ts +1 -1
- package/dist/tools/get-example.d.ts.map +1 -1
- package/dist/tools/get-example.js +76 -0
- package/dist/tools/validate-template.d.ts.map +1 -1
- package/dist/tools/validate-template.js +35 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const create_site_1 = require("./tools/create-site");
|
|
|
15
15
|
const deploy_package_1 = require("./tools/deploy-package");
|
|
16
16
|
const get_field_types_1 = require("./tools/get-field-types");
|
|
17
17
|
const sync_schema_1 = require("./tools/sync-schema");
|
|
18
|
+
const generate_samples_1 = require("./tools/generate-samples");
|
|
18
19
|
const server = new index_js_1.Server({
|
|
19
20
|
name: 'multisite-cms',
|
|
20
21
|
version: '1.0.0',
|
|
@@ -119,6 +120,7 @@ const TOOLS = [
|
|
|
119
120
|
'equality_comparison',
|
|
120
121
|
'youtube_embed',
|
|
121
122
|
'nested_collection_loop',
|
|
123
|
+
'loop_variables',
|
|
122
124
|
],
|
|
123
125
|
description: 'The type of example to retrieve',
|
|
124
126
|
},
|
|
@@ -290,6 +292,25 @@ const TOOLS = [
|
|
|
290
292
|
required: ['projectId'],
|
|
291
293
|
},
|
|
292
294
|
},
|
|
295
|
+
{
|
|
296
|
+
name: 'generate_sample_items',
|
|
297
|
+
description: 'Generate sample items for collections with placeholder content. Automatically handles dependency ordering - collections with relation fields are populated after their parent collections. Use this after creating new collections with sync_schema to preview how they will look. Requires authentication.',
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {
|
|
301
|
+
projectId: {
|
|
302
|
+
type: 'string',
|
|
303
|
+
description: 'Project ID (UUID) or project name. Use list_projects to see available projects.',
|
|
304
|
+
},
|
|
305
|
+
collectionSlugs: {
|
|
306
|
+
type: 'array',
|
|
307
|
+
items: { type: 'string' },
|
|
308
|
+
description: 'Optional: Specific collection slugs to generate samples for. If not provided, generates samples for all empty collections.',
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
required: ['projectId'],
|
|
312
|
+
},
|
|
313
|
+
},
|
|
293
314
|
];
|
|
294
315
|
// Handle list tools request
|
|
295
316
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
@@ -349,6 +370,12 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
349
370
|
fieldsToAdd: params.fieldsToAdd,
|
|
350
371
|
});
|
|
351
372
|
break;
|
|
373
|
+
case 'generate_sample_items':
|
|
374
|
+
result = await (0, generate_samples_1.generateSampleItems)({
|
|
375
|
+
projectId: params.projectId,
|
|
376
|
+
collectionSlugs: params.collectionSlugs,
|
|
377
|
+
});
|
|
378
|
+
break;
|
|
352
379
|
default:
|
|
353
380
|
return {
|
|
354
381
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Sample Items Tool
|
|
3
|
+
*
|
|
4
|
+
* Generates sample items for collections with automatic dependency ordering.
|
|
5
|
+
* Handles relation fields by generating parent collections first.
|
|
6
|
+
* Requires authentication.
|
|
7
|
+
*/
|
|
8
|
+
interface GenerateSamplesInput {
|
|
9
|
+
projectId: string;
|
|
10
|
+
collectionSlugs?: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate sample items for collections
|
|
14
|
+
*
|
|
15
|
+
* @param input - The input with projectId and optional collectionSlugs
|
|
16
|
+
*/
|
|
17
|
+
export declare function generateSampleItems(input: GenerateSamplesInput): Promise<string>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=generate-samples.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-samples.d.ts","sourceRoot":"","sources":["../../src/tools/generate-samples.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAgCH,UAAU,oBAAoB;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAmID;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoLtF"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Generate Sample Items Tool
|
|
4
|
+
*
|
|
5
|
+
* Generates sample items for collections with automatic dependency ordering.
|
|
6
|
+
* Handles relation fields by generating parent collections first.
|
|
7
|
+
* Requires authentication.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.generateSampleItems = generateSampleItems;
|
|
11
|
+
const api_client_1 = require("../lib/api-client");
|
|
12
|
+
const device_flow_1 = require("../lib/device-flow");
|
|
13
|
+
// ============ Constants ============
|
|
14
|
+
const AUTH_REQUIRED_MESSAGE = `# Authentication Required
|
|
15
|
+
|
|
16
|
+
This tool requires authentication to generate sample items.
|
|
17
|
+
|
|
18
|
+
**To authenticate:**
|
|
19
|
+
1. Set the FASTMODE_AUTH_TOKEN environment variable, OR
|
|
20
|
+
2. Run this tool again and follow the browser-based login flow
|
|
21
|
+
|
|
22
|
+
Use \`list_projects\` to verify your authentication status.
|
|
23
|
+
`;
|
|
24
|
+
// ============ Helper Functions ============
|
|
25
|
+
/**
|
|
26
|
+
* Resolve project identifier to tenant ID
|
|
27
|
+
*/
|
|
28
|
+
async function resolveProjectId(projectIdentifier) {
|
|
29
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
30
|
+
if (uuidPattern.test(projectIdentifier)) {
|
|
31
|
+
return { tenantId: projectIdentifier };
|
|
32
|
+
}
|
|
33
|
+
const response = await (0, api_client_1.apiRequest)('/api/tenants');
|
|
34
|
+
if ((0, api_client_1.isApiError)(response)) {
|
|
35
|
+
return { error: `Failed to look up project: ${response.error}` };
|
|
36
|
+
}
|
|
37
|
+
const match = response.data.find(p => p.name.toLowerCase() === projectIdentifier.toLowerCase());
|
|
38
|
+
if (match) {
|
|
39
|
+
return { tenantId: match.id };
|
|
40
|
+
}
|
|
41
|
+
const partialMatch = response.data.find(p => p.name.toLowerCase().includes(projectIdentifier.toLowerCase()));
|
|
42
|
+
if (partialMatch) {
|
|
43
|
+
return { tenantId: partialMatch.id };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
error: `Project "${projectIdentifier}" not found. Use list_projects to see available projects.`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Topological sort of collections based on relation dependencies
|
|
51
|
+
* Returns collections ordered so that dependencies come first
|
|
52
|
+
*/
|
|
53
|
+
function sortByDependencies(collections) {
|
|
54
|
+
const slugToCollection = new Map();
|
|
55
|
+
const inDegree = new Map();
|
|
56
|
+
const adjacencyList = new Map();
|
|
57
|
+
// Initialize
|
|
58
|
+
for (const col of collections) {
|
|
59
|
+
slugToCollection.set(col.slug, col);
|
|
60
|
+
inDegree.set(col.slug, 0);
|
|
61
|
+
adjacencyList.set(col.slug, []);
|
|
62
|
+
}
|
|
63
|
+
// Build dependency graph
|
|
64
|
+
// If collection A has a relation to collection B, then B must be populated first
|
|
65
|
+
// So B -> A (B is a dependency of A)
|
|
66
|
+
for (const col of collections) {
|
|
67
|
+
for (const field of col.fields) {
|
|
68
|
+
if (field.type === 'relation' && field.referenceCollection) {
|
|
69
|
+
const refSlug = field.referenceCollection;
|
|
70
|
+
// Only count if the referenced collection is in our list
|
|
71
|
+
if (slugToCollection.has(refSlug)) {
|
|
72
|
+
// refSlug must come before col.slug
|
|
73
|
+
const deps = adjacencyList.get(refSlug) || [];
|
|
74
|
+
deps.push(col.slug);
|
|
75
|
+
adjacencyList.set(refSlug, deps);
|
|
76
|
+
inDegree.set(col.slug, (inDegree.get(col.slug) || 0) + 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Kahn's algorithm for topological sort
|
|
82
|
+
const queue = [];
|
|
83
|
+
const result = [];
|
|
84
|
+
// Start with nodes that have no dependencies
|
|
85
|
+
for (const [slug, degree] of inDegree) {
|
|
86
|
+
if (degree === 0) {
|
|
87
|
+
queue.push(slug);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
while (queue.length > 0) {
|
|
91
|
+
const current = queue.shift();
|
|
92
|
+
const col = slugToCollection.get(current);
|
|
93
|
+
if (col) {
|
|
94
|
+
result.push(col);
|
|
95
|
+
}
|
|
96
|
+
// Reduce in-degree for dependent collections
|
|
97
|
+
const dependents = adjacencyList.get(current) || [];
|
|
98
|
+
for (const dep of dependents) {
|
|
99
|
+
const newDegree = (inDegree.get(dep) || 0) - 1;
|
|
100
|
+
inDegree.set(dep, newDegree);
|
|
101
|
+
if (newDegree === 0) {
|
|
102
|
+
queue.push(dep);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// If there are still collections not in result (cycle detected), add them anyway
|
|
107
|
+
for (const col of collections) {
|
|
108
|
+
if (!result.find(c => c.slug === col.slug)) {
|
|
109
|
+
result.push(col);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
// ============ Main Function ============
|
|
115
|
+
/**
|
|
116
|
+
* Generate sample items for collections
|
|
117
|
+
*
|
|
118
|
+
* @param input - The input with projectId and optional collectionSlugs
|
|
119
|
+
*/
|
|
120
|
+
async function generateSampleItems(input) {
|
|
121
|
+
// Check authentication
|
|
122
|
+
if (await (0, api_client_1.needsAuthentication)()) {
|
|
123
|
+
const authResult = await (0, device_flow_1.ensureAuthenticated)();
|
|
124
|
+
if (!authResult.authenticated) {
|
|
125
|
+
return AUTH_REQUIRED_MESSAGE;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const { projectId, collectionSlugs } = input;
|
|
129
|
+
// Validate input
|
|
130
|
+
if (!projectId) {
|
|
131
|
+
return `# Error: Missing projectId
|
|
132
|
+
|
|
133
|
+
Please provide a projectId. Use \`list_projects\` to see your available projects.
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
// Resolve project ID
|
|
137
|
+
const resolved = await resolveProjectId(projectId);
|
|
138
|
+
if ('error' in resolved) {
|
|
139
|
+
return `# Project Not Found
|
|
140
|
+
|
|
141
|
+
${resolved.error}
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
const { tenantId } = resolved;
|
|
145
|
+
// Fetch all collections for this project
|
|
146
|
+
const collectionsRes = await (0, api_client_1.apiRequest)('/api/collections', { tenantId });
|
|
147
|
+
if ((0, api_client_1.isApiError)(collectionsRes)) {
|
|
148
|
+
// Check if auth error
|
|
149
|
+
if (collectionsRes.error.includes('401') || collectionsRes.error.includes('auth')) {
|
|
150
|
+
const authResult = await (0, device_flow_1.ensureAuthenticated)();
|
|
151
|
+
if (!authResult.authenticated) {
|
|
152
|
+
return AUTH_REQUIRED_MESSAGE;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return `# Error
|
|
156
|
+
|
|
157
|
+
Failed to fetch collections: ${collectionsRes.error}
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
const allCollections = collectionsRes.data;
|
|
161
|
+
if (allCollections.length === 0) {
|
|
162
|
+
return `# No Collections Found
|
|
163
|
+
|
|
164
|
+
This project has no collections yet. Use \`sync_schema\` to create collections first.
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
// Determine which collections to generate samples for
|
|
168
|
+
let targetCollections;
|
|
169
|
+
if (collectionSlugs && collectionSlugs.length > 0) {
|
|
170
|
+
// Specific collections requested
|
|
171
|
+
targetCollections = [];
|
|
172
|
+
const notFound = [];
|
|
173
|
+
for (const slug of collectionSlugs) {
|
|
174
|
+
const col = allCollections.find(c => c.slug.toLowerCase() === slug.toLowerCase());
|
|
175
|
+
if (col) {
|
|
176
|
+
targetCollections.push(col);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
notFound.push(slug);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (notFound.length > 0) {
|
|
183
|
+
return `# Collections Not Found
|
|
184
|
+
|
|
185
|
+
The following collections were not found: ${notFound.join(', ')}
|
|
186
|
+
|
|
187
|
+
Available collections: ${allCollections.map(c => c.slug).join(', ')}
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// All collections - check which ones are empty
|
|
193
|
+
const emptyCollections = [];
|
|
194
|
+
for (const col of allCollections) {
|
|
195
|
+
const itemsRes = await (0, api_client_1.apiRequest)(`/api/collections/${col.id}/items`, { tenantId });
|
|
196
|
+
if (!(0, api_client_1.isApiError)(itemsRes) && itemsRes.data.length === 0) {
|
|
197
|
+
emptyCollections.push(col);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (emptyCollections.length === 0) {
|
|
201
|
+
return `# No Empty Collections
|
|
202
|
+
|
|
203
|
+
All collections already have items. To regenerate samples for specific collections, provide their slugs.
|
|
204
|
+
|
|
205
|
+
Collections with items: ${allCollections.map(c => c.slug).join(', ')}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
targetCollections = emptyCollections;
|
|
209
|
+
}
|
|
210
|
+
// Sort collections by dependencies (parents first)
|
|
211
|
+
const sortedCollections = sortByDependencies(targetCollections);
|
|
212
|
+
// Generate samples for each collection in order
|
|
213
|
+
const results = [];
|
|
214
|
+
const generated = [];
|
|
215
|
+
const failed = [];
|
|
216
|
+
results.push('## Generation Order\n');
|
|
217
|
+
results.push('Collections are processed in dependency order (parent collections first):\n');
|
|
218
|
+
results.push(sortedCollections.map((c, i) => `${i + 1}. ${c.name} (\`${c.slug}\`)`).join('\n'));
|
|
219
|
+
results.push('\n');
|
|
220
|
+
for (const col of sortedCollections) {
|
|
221
|
+
// Check for relation fields and their dependencies
|
|
222
|
+
const relationFields = col.fields.filter(f => f.type === 'relation' && f.referenceCollection);
|
|
223
|
+
const dependencies = relationFields.map(f => f.referenceCollection).filter(Boolean);
|
|
224
|
+
let dependencyNote = '';
|
|
225
|
+
if (dependencies.length > 0) {
|
|
226
|
+
dependencyNote = ` (depends on: ${dependencies.join(', ')})`;
|
|
227
|
+
}
|
|
228
|
+
// Call the generate-samples endpoint
|
|
229
|
+
const generateRes = await (0, api_client_1.apiRequest)(`/api/collections/${col.id}/generate-samples`, {
|
|
230
|
+
tenantId,
|
|
231
|
+
method: 'POST',
|
|
232
|
+
});
|
|
233
|
+
if ((0, api_client_1.isApiError)(generateRes)) {
|
|
234
|
+
failed.push({ collection: col.slug, error: generateRes.error });
|
|
235
|
+
results.push(`❌ **${col.name}** - Failed: ${generateRes.error}${dependencyNote}`);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const count = generateRes.data.data?.length || 5;
|
|
239
|
+
generated.push({ collection: col.slug, count });
|
|
240
|
+
results.push(`✅ **${col.name}** - Generated ${count} sample items${dependencyNote}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Build summary
|
|
244
|
+
let output = `# Sample Items Generated
|
|
245
|
+
|
|
246
|
+
**Project ID:** \`${tenantId}\`
|
|
247
|
+
|
|
248
|
+
## Summary
|
|
249
|
+
|
|
250
|
+
| Metric | Count |
|
|
251
|
+
|--------|-------|
|
|
252
|
+
| Collections Processed | ${sortedCollections.length} |
|
|
253
|
+
| Successfully Generated | ${generated.length} |
|
|
254
|
+
| Failed | ${failed.length} |
|
|
255
|
+
|
|
256
|
+
${results.join('\n')}
|
|
257
|
+
`;
|
|
258
|
+
if (failed.length > 0) {
|
|
259
|
+
output += `\n## Errors\n\n`;
|
|
260
|
+
for (const f of failed) {
|
|
261
|
+
output += `- **${f.collection}**: ${f.error}\n`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (generated.length > 0) {
|
|
265
|
+
output += `\n---\n\n**Next Steps:**
|
|
266
|
+
- View the generated items in the CMS dashboard
|
|
267
|
+
- Edit or delete sample items as needed
|
|
268
|
+
- These are placeholder items - replace with real content when ready
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
return output;
|
|
272
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-conversion-guide.d.ts","sourceRoot":"","sources":["../../src/tools/get-conversion-guide.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,MAAM,GAAG,aAAa,GAAG,UAAU,GAAG,WAAW,GAAG,KAAK,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"get-conversion-guide.d.ts","sourceRoot":"","sources":["../../src/tools/get-conversion-guide.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,MAAM,GAAG,aAAa,GAAG,UAAU,GAAG,WAAW,GAAG,KAAK,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AAu4BlJ;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CA2D1E"}
|
|
@@ -36,7 +36,9 @@ Based on what \`list_projects\` returns:
|
|
|
36
36
|
2. Call \`create_site(name)\` to create the project
|
|
37
37
|
3. You'll receive a projectId to use for deployment
|
|
38
38
|
4. After building the package, call \`sync_schema\` to create collections/fields
|
|
39
|
-
5.
|
|
39
|
+
5. **Ask the user:** "Would you like me to generate sample items so you can preview how the collections will look?"
|
|
40
|
+
6. If yes, call \`generate_sample_items(projectId)\` - it handles dependency ordering automatically
|
|
41
|
+
7. Proceed to Phase 1 (Website Analysis)
|
|
40
42
|
|
|
41
43
|
---
|
|
42
44
|
|
|
@@ -577,10 +579,32 @@ Use \`@root.\` to access collections from the root context when you need to be e
|
|
|
577
579
|
\`\`\`
|
|
578
580
|
|
|
579
581
|
## Loop Variables
|
|
580
|
-
Inside {{#each}}:
|
|
582
|
+
Inside {{#each}} loops, these special variables are available:
|
|
583
|
+
|
|
584
|
+
**Standalone tokens:**
|
|
581
585
|
- \`{{@first}}\` - true for first item
|
|
582
586
|
- \`{{@last}}\` - true for last item
|
|
583
|
-
- \`{{@index}}\` - zero-based index
|
|
587
|
+
- \`{{@index}}\` - zero-based index (0, 1, 2...)
|
|
588
|
+
- \`{{@length}}\` - total number of items
|
|
589
|
+
|
|
590
|
+
**Conditional usage:**
|
|
591
|
+
\`\`\`html
|
|
592
|
+
{{#each posts}}
|
|
593
|
+
{{#if @first}}<div class="featured">{{/if}}
|
|
594
|
+
{{#unless @first}}<hr class="separator">{{/unless}}
|
|
595
|
+
<article>{{name}}</article>
|
|
596
|
+
{{#unless @last}},{{/unless}}
|
|
597
|
+
{{/each}}
|
|
598
|
+
\`\`\`
|
|
599
|
+
|
|
600
|
+
| Pattern | Use Case |
|
|
601
|
+
|---------|----------|
|
|
602
|
+
| \`{{#if @first}}\` | Style first item differently |
|
|
603
|
+
| \`{{#unless @first}}\` | Add separator before all but first |
|
|
604
|
+
| \`{{#if @last}}\` | Style last item differently |
|
|
605
|
+
| \`{{#unless @last}}\` | Add comma/separator after all but last |
|
|
606
|
+
|
|
607
|
+
**Important:** Loop variables only work inside \`{{#each}}\` blocks. Using them outside a loop will not work.
|
|
584
608
|
|
|
585
609
|
## Parent Context (\`../\`)
|
|
586
610
|
Inside loops, access the parent scope:
|
|
@@ -923,6 +947,7 @@ Use these MCP tools during conversion:
|
|
|
923
947
|
- \`validate_manifest\` - Check your manifest.json
|
|
924
948
|
- \`validate_template\` - Check template token usage
|
|
925
949
|
- \`validate_package\` - Full package validation
|
|
950
|
+
- \`generate_sample_items\` - Create placeholder items after creating new collections (handles relation dependencies automatically)
|
|
926
951
|
|
|
927
952
|
**Validate as you work** - don't wait until the end!`;
|
|
928
953
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type ExampleType = 'manifest_basic' | 'manifest_custom_paths' | 'manifest_minimal_with_ui' | 'blog_index_template' | 'blog_post_template' | 'team_template' | 'downloads_template' | 'authors_template' | 'author_detail_template' | 'custom_collection_template' | 'form_handling' | 'asset_paths' | 'image_handling' | 'relation_fields' | 'data_edit_keys' | 'each_loop' | 'conditional_if' | 'nested_fields' | 'featured_posts' | 'parent_context' | 'equality_comparison' | 'youtube_embed' | 'nested_collection_loop';
|
|
1
|
+
type ExampleType = 'manifest_basic' | 'manifest_custom_paths' | 'manifest_minimal_with_ui' | 'blog_index_template' | 'blog_post_template' | 'team_template' | 'downloads_template' | 'authors_template' | 'author_detail_template' | 'custom_collection_template' | 'form_handling' | 'asset_paths' | 'image_handling' | 'relation_fields' | 'data_edit_keys' | 'each_loop' | 'conditional_if' | 'nested_fields' | 'featured_posts' | 'parent_context' | 'equality_comparison' | 'youtube_embed' | 'nested_collection_loop' | 'loop_variables';
|
|
2
2
|
/**
|
|
3
3
|
* Returns example code for a specific pattern
|
|
4
4
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GACZ,gBAAgB,GAChB,uBAAuB,GACvB,0BAA0B,GAC1B,qBAAqB,GACrB,oBAAoB,GACpB,eAAe,GACf,oBAAoB,GACpB,kBAAkB,GAClB,wBAAwB,GACxB,4BAA4B,GAC5B,eAAe,GACf,aAAa,GACb,gBAAgB,GAChB,iBAAiB,GACjB,gBAAgB,GAChB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,eAAe,GACf,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GACZ,gBAAgB,GAChB,uBAAuB,GACvB,0BAA0B,GAC1B,qBAAqB,GACrB,oBAAoB,GACpB,eAAe,GACf,oBAAoB,GACpB,kBAAkB,GAClB,wBAAwB,GACxB,4BAA4B,GAC5B,eAAe,GACf,aAAa,GACb,gBAAgB,GAChB,iBAAiB,GACjB,gBAAgB,GAChB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,eAAe,GACf,wBAAwB,GACxB,gBAAgB,CAAC;AAgvCrB;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
|
|
@@ -1162,6 +1162,82 @@ This sidebar pattern can be included on:
|
|
|
1162
1162
|
This gives you URLs like:
|
|
1163
1163
|
- \`/docs\` - Documentation index showing all categories with their pages
|
|
1164
1164
|
- \`/docs/quick-start-guide\` - Individual doc page`,
|
|
1165
|
+
loop_variables: `# Loop Variables
|
|
1166
|
+
|
|
1167
|
+
Special variables available inside \`{{#each}}\` loops.
|
|
1168
|
+
|
|
1169
|
+
## Available Variables
|
|
1170
|
+
|
|
1171
|
+
| Variable | Description |
|
|
1172
|
+
|----------|-------------|
|
|
1173
|
+
| \`{{@index}}\` | Zero-based index (0, 1, 2...) |
|
|
1174
|
+
| \`{{@first}}\` | True for first item only |
|
|
1175
|
+
| \`{{@last}}\` | True for last item only |
|
|
1176
|
+
| \`{{@length}}\` | Total number of items |
|
|
1177
|
+
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
## Conditional Usage
|
|
1181
|
+
|
|
1182
|
+
Use loop variables in conditionals for styling:
|
|
1183
|
+
|
|
1184
|
+
\`\`\`html
|
|
1185
|
+
{{#each team_members}}
|
|
1186
|
+
{{#if @first}}
|
|
1187
|
+
<div class="featured-member">{{name}} - Team Lead</div>
|
|
1188
|
+
{{else}}
|
|
1189
|
+
<div class="member">{{name}}</div>
|
|
1190
|
+
{{/if}}
|
|
1191
|
+
{{/each}}
|
|
1192
|
+
\`\`\`
|
|
1193
|
+
|
|
1194
|
+
---
|
|
1195
|
+
|
|
1196
|
+
## Common Patterns
|
|
1197
|
+
|
|
1198
|
+
### Add separator between items (not before first):
|
|
1199
|
+
\`\`\`html
|
|
1200
|
+
{{#each tags}}
|
|
1201
|
+
{{#unless @first}} | {{/unless}}
|
|
1202
|
+
<span>{{name}}</span>
|
|
1203
|
+
{{/each}}
|
|
1204
|
+
\`\`\`
|
|
1205
|
+
Output: \`Tag1 | Tag2 | Tag3\`
|
|
1206
|
+
|
|
1207
|
+
### Comma-separated list (no comma after last):
|
|
1208
|
+
\`\`\`html
|
|
1209
|
+
{{#each authors}}
|
|
1210
|
+
<span>{{name}}</span>{{#unless @last}}, {{/unless}}
|
|
1211
|
+
{{/each}}
|
|
1212
|
+
\`\`\`
|
|
1213
|
+
Output: \`Alice, Bob, Charlie\`
|
|
1214
|
+
|
|
1215
|
+
### Style first and last items:
|
|
1216
|
+
\`\`\`html
|
|
1217
|
+
{{#each posts}}
|
|
1218
|
+
<article class="{{#if @first}}first{{/if}} {{#if @last}}last{{/if}}">
|
|
1219
|
+
{{name}}
|
|
1220
|
+
</article>
|
|
1221
|
+
{{/each}}
|
|
1222
|
+
\`\`\`
|
|
1223
|
+
|
|
1224
|
+
### Show position number:
|
|
1225
|
+
\`\`\`html
|
|
1226
|
+
{{#each leaderboard}}
|
|
1227
|
+
<div class="rank-{{@index}}">
|
|
1228
|
+
#{{@index}}: {{name}} - {{score}} points
|
|
1229
|
+
</div>
|
|
1230
|
+
{{/each}}
|
|
1231
|
+
\`\`\`
|
|
1232
|
+
|
|
1233
|
+
---
|
|
1234
|
+
|
|
1235
|
+
## Important Notes
|
|
1236
|
+
|
|
1237
|
+
- Loop variables **only work inside \`{{#each}}\`** blocks
|
|
1238
|
+
- Using them outside a loop will not work and will log a warning
|
|
1239
|
+
- \`{{#unless @first}}\` is the opposite of \`{{#if @first}}\`
|
|
1240
|
+
- \`{{#unless @last}}\` is the opposite of \`{{#if @last}}\``,
|
|
1165
1241
|
};
|
|
1166
1242
|
/**
|
|
1167
1243
|
* Returns example code for a specific pattern
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-template.d.ts","sourceRoot":"","sources":["../../src/tools/validate-template.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-template.d.ts","sourceRoot":"","sources":["../../src/tools/validate-template.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;AAuTrE;;;;;;;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,CAkejB"}
|
|
@@ -205,6 +205,36 @@ function extractCollectionReferences(html) {
|
|
|
205
205
|
}
|
|
206
206
|
return [...new Set(collections)]; // Unique collections
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Validate that loop variables (@first, @last, @index) are only used inside {{#each}} blocks
|
|
210
|
+
* Returns warnings for any usage found outside loops
|
|
211
|
+
*/
|
|
212
|
+
function validateLoopVariables(html) {
|
|
213
|
+
const warnings = [];
|
|
214
|
+
// Find all @first, @last, @index, @length usage (standalone or in conditionals)
|
|
215
|
+
const loopVarPattern = /\{\{[#/]?(?:if|unless)?\s*@(first|last|index|length)[^}]*\}\}/g;
|
|
216
|
+
// Find all {{#each}}...{{/each}} blocks
|
|
217
|
+
// Use a simple approach: remove all loop content and check what's left
|
|
218
|
+
let outsideLoops = html;
|
|
219
|
+
// Repeatedly remove innermost loops until none left
|
|
220
|
+
// This handles nested loops correctly
|
|
221
|
+
let previousLength = 0;
|
|
222
|
+
while (outsideLoops.length !== previousLength) {
|
|
223
|
+
previousLength = outsideLoops.length;
|
|
224
|
+
// Match non-greedy: find {{#each...}} followed by content without nested {{#each}}, then {{/each}}
|
|
225
|
+
outsideLoops = outsideLoops.replace(/\{\{#each[^}]*\}\}(?:(?!\{\{#each)[\s\S])*?\{\{\/each\}\}/g, '');
|
|
226
|
+
}
|
|
227
|
+
// Now check if any loop variables remain in the content outside loops
|
|
228
|
+
const matches = outsideLoops.match(loopVarPattern);
|
|
229
|
+
if (matches && matches.length > 0) {
|
|
230
|
+
const uniqueVars = [...new Set(matches.map(m => {
|
|
231
|
+
const varMatch = m.match(/@(first|last|index|length)/);
|
|
232
|
+
return varMatch ? `@${varMatch[1]}` : m;
|
|
233
|
+
}))];
|
|
234
|
+
warnings.push(`- Loop variables (${uniqueVars.join(', ')}) found outside {{#each}} blocks. These only work inside loops.`);
|
|
235
|
+
}
|
|
236
|
+
return warnings;
|
|
237
|
+
}
|
|
208
238
|
/**
|
|
209
239
|
* Validates an HTML template for correct CMS token usage
|
|
210
240
|
*
|
|
@@ -279,6 +309,11 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
|
279
309
|
if (unlessOpens !== unlessCloses) {
|
|
280
310
|
errors.push(`- Unbalanced {{#unless}}: ${unlessOpens} opens, ${unlessCloses} closes`);
|
|
281
311
|
}
|
|
312
|
+
// Check for loop variables (@first, @last, @index) used outside {{#each}} blocks
|
|
313
|
+
const loopVarWarnings = validateLoopVariables(html);
|
|
314
|
+
for (const warning of loopVarWarnings) {
|
|
315
|
+
warnings.push(warning);
|
|
316
|
+
}
|
|
282
317
|
// Check for data-edit-key in static pages
|
|
283
318
|
if (templateType === 'static_page') {
|
|
284
319
|
const editKeys = html.match(/data-edit-key="[^"]+"/g) || [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multisite-cms-mcp",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.9",
|
|
4
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": {
|