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 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;AA+2BlJ;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CA0D1E"}
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. Proceed to Phase 1 (Website Analysis)
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;AAmqC7B;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
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;AAmRrE;;;;;;;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,CA4djB"}
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.7",
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": {