fastmode-mcp 1.0.1 → 1.0.2
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/bin/run.js +50 -0
- package/dist/index.js +31 -2
- package/dist/lib/credentials.d.ts.map +1 -1
- package/dist/lib/device-flow.d.ts.map +1 -1
- package/dist/tools/cms-items.d.ts +9 -0
- package/dist/tools/cms-items.d.ts.map +1 -1
- package/dist/tools/cms-items.js +204 -9
- package/dist/tools/get-started.d.ts.map +1 -1
- package/dist/tools/get-started.js +81 -5
- package/package.json +5 -4
- package/scripts/postinstall.js +129 -0
- package/dist/tools/get-schema.d.ts +0 -5
- package/dist/tools/get-schema.d.ts.map +0 -1
- package/dist/tools/get-schema.js +0 -320
package/bin/run.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Entry point that tries to run the native binary first, then falls back to Node.js.
|
|
4
|
+
* This allows the MCP server to work regardless of how Node.js is installed.
|
|
5
|
+
*/
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const BINARY_NAME = 'fastmode-mcp';
|
|
11
|
+
|
|
12
|
+
function getNativeBinaryPath() {
|
|
13
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
14
|
+
const binaryPath = path.join(__dirname, `${BINARY_NAME}${ext}`);
|
|
15
|
+
|
|
16
|
+
if (fs.existsSync(binaryPath)) {
|
|
17
|
+
return binaryPath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runNativeBinary(binaryPath) {
|
|
24
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
25
|
+
stdio: 'inherit',
|
|
26
|
+
env: process.env,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
child.on('error', (error) => {
|
|
30
|
+
console.error(`Failed to run native binary: ${error.message}`);
|
|
31
|
+
runNodeFallback();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.on('exit', (code) => {
|
|
35
|
+
process.exit(code || 0);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runNodeFallback() {
|
|
40
|
+
// Fall back to the Node.js version
|
|
41
|
+
require('../dist/index.js');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const nativeBinary = getNativeBinaryPath();
|
|
45
|
+
|
|
46
|
+
if (nativeBinary) {
|
|
47
|
+
runNativeBinary(nativeBinary);
|
|
48
|
+
} else {
|
|
49
|
+
runNodeFallback();
|
|
50
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -373,7 +373,7 @@ const TOOLS = [
|
|
|
373
373
|
},
|
|
374
374
|
{
|
|
375
375
|
name: 'create_cms_item',
|
|
376
|
-
description: 'FIRST: Call get_started(intent: "add_content") to see collections
|
|
376
|
+
description: 'FIRST: Call get_started(intent: "add_content") to see collections, field types, AND relation field options with IDs. Creates a new CMS item. For relation fields, you MUST use item IDs (not names) - the response will warn you if relation fields were left empty and show available options. For richText fields, use HTML content. Requires authentication.',
|
|
377
377
|
inputSchema: {
|
|
378
378
|
type: 'object',
|
|
379
379
|
properties: {
|
|
@@ -460,7 +460,7 @@ const TOOLS = [
|
|
|
460
460
|
},
|
|
461
461
|
{
|
|
462
462
|
name: 'update_cms_item',
|
|
463
|
-
description: 'FIRST: Call get_started(intent: "add_content") to see collections
|
|
463
|
+
description: 'FIRST: Call get_started(intent: "add_content") to see collections, field types, AND relation field options with IDs. Updates an existing CMS item. Only provided fields are changed. For relation fields, use item IDs from get_relation_options or list_cms_items. Requires authentication.',
|
|
464
464
|
inputSchema: {
|
|
465
465
|
type: 'object',
|
|
466
466
|
properties: {
|
|
@@ -518,6 +518,28 @@ const TOOLS = [
|
|
|
518
518
|
required: ['projectId', 'collectionSlug', 'itemSlug', 'confirmDelete'],
|
|
519
519
|
},
|
|
520
520
|
},
|
|
521
|
+
{
|
|
522
|
+
name: 'get_relation_options',
|
|
523
|
+
description: 'Get available options (item IDs) for relation fields in a collection. Use this to see what items can be linked via relation fields. Shows item IDs, names, and slugs for easy reference when creating or updating items.',
|
|
524
|
+
inputSchema: {
|
|
525
|
+
type: 'object',
|
|
526
|
+
properties: {
|
|
527
|
+
projectId: {
|
|
528
|
+
type: 'string',
|
|
529
|
+
description: 'Project ID (UUID) or project name.',
|
|
530
|
+
},
|
|
531
|
+
collectionSlug: {
|
|
532
|
+
type: 'string',
|
|
533
|
+
description: 'The collection that has relation fields (e.g., "posts" if posts have an author field).',
|
|
534
|
+
},
|
|
535
|
+
fieldSlug: {
|
|
536
|
+
type: 'string',
|
|
537
|
+
description: 'Optional: Specific relation field to get options for. If not provided, shows all relation fields.',
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
required: ['projectId', 'collectionSlug'],
|
|
541
|
+
},
|
|
542
|
+
},
|
|
521
543
|
];
|
|
522
544
|
// Handle list tools request
|
|
523
545
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
@@ -633,6 +655,13 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
633
655
|
confirmDelete: params.confirmDelete,
|
|
634
656
|
});
|
|
635
657
|
break;
|
|
658
|
+
case 'get_relation_options':
|
|
659
|
+
result = await (0, cms_items_1.getRelationOptions)({
|
|
660
|
+
projectId: params.projectId,
|
|
661
|
+
collectionSlug: params.collectionSlug,
|
|
662
|
+
fieldSlug: params.fieldSlug,
|
|
663
|
+
});
|
|
664
|
+
break;
|
|
636
665
|
default:
|
|
637
666
|
return {
|
|
638
667
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C;AAYD;;GAEG;AACH,wBAAgB,eAAe,IAAI,iBAAiB,GAAG,IAAI,CAoB1D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAUpE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAUxC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,iBAAiB,EAAE,aAAa,GAAE,MAAU,GAAG,OAAO,CAIjG;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C;AAYD;;GAEG;AACH,wBAAgB,eAAe,IAAI,iBAAiB,GAAG,IAAI,CAoB1D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAUpE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAUxC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,iBAAiB,EAAE,aAAa,GAAE,MAAU,GAAG,OAAO,CAIjG;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAkD1G;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAoB7E;AAED;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAG3D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/lib/device-flow.ts"],"names":[],"mappings":"AA0DA;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAqGvD;
|
|
1
|
+
{"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/lib/device-flow.ts"],"names":[],"mappings":"AA0DA;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAqGvD;AA8ED;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA2BhG"}
|
|
@@ -53,4 +53,13 @@ export declare function deleteCmsItem(params: {
|
|
|
53
53
|
itemSlug: string;
|
|
54
54
|
confirmDelete: boolean;
|
|
55
55
|
}): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Get relation field options for a collection
|
|
58
|
+
* Shows all relation fields and their available values
|
|
59
|
+
*/
|
|
60
|
+
export declare function getRelationOptions(params: {
|
|
61
|
+
projectId: string;
|
|
62
|
+
collectionSlug: string;
|
|
63
|
+
fieldSlug?: string;
|
|
64
|
+
}): Promise<string>;
|
|
56
65
|
//# sourceMappingURL=cms-items.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cms-items.d.ts","sourceRoot":"","sources":["../../src/tools/cms-items.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"cms-items.d.ts","sourceRoot":"","sources":["../../src/tools/cms-items.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA4NH;;GAEG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2DlB;AA+DD;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6ClB;AA4CD;;GAEG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6ClB;AAoBD;;GAEG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,GAAG,OAAO,CAAC,MAAM,CAAC,CA6ElB;AAkBD;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwElB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0FlB"}
|
package/dist/tools/cms-items.js
CHANGED
|
@@ -10,8 +10,15 @@ exports.listCmsItems = listCmsItems;
|
|
|
10
10
|
exports.getCmsItem = getCmsItem;
|
|
11
11
|
exports.updateCmsItem = updateCmsItem;
|
|
12
12
|
exports.deleteCmsItem = deleteCmsItem;
|
|
13
|
+
exports.getRelationOptions = getRelationOptions;
|
|
13
14
|
const api_client_1 = require("../lib/api-client");
|
|
14
15
|
const device_flow_1 = require("../lib/device-flow");
|
|
16
|
+
/**
|
|
17
|
+
* Type guard to check if prepare result failed
|
|
18
|
+
*/
|
|
19
|
+
function prepFailed(prep) {
|
|
20
|
+
return !prep.success;
|
|
21
|
+
}
|
|
15
22
|
/**
|
|
16
23
|
* Helper to ensure authentication and resolve project ID
|
|
17
24
|
*/
|
|
@@ -48,12 +55,77 @@ async function getCollectionId(tenantId, collectionSlug) {
|
|
|
48
55
|
}
|
|
49
56
|
return { collectionId: collection.id };
|
|
50
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Get collection with full field details
|
|
60
|
+
*/
|
|
61
|
+
async function getCollectionWithFields(tenantId, collectionSlug) {
|
|
62
|
+
const response = await (0, api_client_1.apiRequest)('/api/collections', { tenantId });
|
|
63
|
+
if ((0, api_client_1.isApiError)(response)) {
|
|
64
|
+
return { error: `Failed to fetch collections: ${response.error}` };
|
|
65
|
+
}
|
|
66
|
+
const collection = response.data.find(c => c.slug === collectionSlug);
|
|
67
|
+
if (!collection) {
|
|
68
|
+
return { error: `Collection "${collectionSlug}" not found.` };
|
|
69
|
+
}
|
|
70
|
+
// The collections endpoint returns fields, cast to proper type
|
|
71
|
+
return { collection: collection };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get items from a collection (for relation options)
|
|
75
|
+
*/
|
|
76
|
+
async function getCollectionItems(tenantId, collectionSlug, limit = 10) {
|
|
77
|
+
// First get collection ID
|
|
78
|
+
const collResult = await getCollectionId(tenantId, collectionSlug);
|
|
79
|
+
if ('error' in collResult) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const response = await (0, api_client_1.apiRequest)(`/api/collections/${collResult.collectionId}/items?limit=${limit}`, { tenantId, method: 'GET' });
|
|
83
|
+
if ((0, api_client_1.isApiError)(response)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return response.data.map(item => ({
|
|
87
|
+
id: item.id,
|
|
88
|
+
name: item.name,
|
|
89
|
+
slug: item.slug,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check for empty relation fields and get their options
|
|
94
|
+
*/
|
|
95
|
+
async function checkEmptyRelations(tenantId, collectionSlug, itemData) {
|
|
96
|
+
const collResult = await getCollectionWithFields(tenantId, collectionSlug);
|
|
97
|
+
if ('error' in collResult) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const { collection } = collResult;
|
|
101
|
+
const emptyRelations = [];
|
|
102
|
+
// Check each relation field
|
|
103
|
+
for (const field of collection.fields || []) {
|
|
104
|
+
if (field.type === 'relation' && field.referenceCollection) {
|
|
105
|
+
const value = itemData[field.slug];
|
|
106
|
+
// Check if the field is empty (null, undefined, or empty string)
|
|
107
|
+
if (value === null || value === undefined || value === '') {
|
|
108
|
+
// Get options from the referenced collection
|
|
109
|
+
const options = await getCollectionItems(tenantId, field.referenceCollection, 10);
|
|
110
|
+
if (options.length > 0) {
|
|
111
|
+
emptyRelations.push({
|
|
112
|
+
fieldSlug: field.slug,
|
|
113
|
+
fieldName: field.name,
|
|
114
|
+
referenceCollection: field.referenceCollection,
|
|
115
|
+
options,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return emptyRelations;
|
|
122
|
+
}
|
|
51
123
|
/**
|
|
52
124
|
* Create a new item in a CMS collection
|
|
53
125
|
*/
|
|
54
126
|
async function createCmsItem(params) {
|
|
55
127
|
const prep = await prepareRequest(params.projectId);
|
|
56
|
-
if (
|
|
128
|
+
if (prepFailed(prep))
|
|
57
129
|
return prep.message;
|
|
58
130
|
// Get collection ID from slug
|
|
59
131
|
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
|
|
@@ -89,14 +161,18 @@ async function createCmsItem(params) {
|
|
|
89
161
|
if ((0, api_client_1.isApiError)(retry)) {
|
|
90
162
|
return `# Error Creating Item\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
|
|
91
163
|
}
|
|
92
|
-
|
|
164
|
+
// Check for empty relation fields
|
|
165
|
+
const emptyRelations = await checkEmptyRelations(prep.tenantId, params.collectionSlug, retry.data.data);
|
|
166
|
+
return formatCreatedItem(retry.data, params.collectionSlug, params.projectId, emptyRelations);
|
|
93
167
|
}
|
|
94
168
|
return `# Error Creating Item\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
|
|
95
169
|
}
|
|
96
|
-
|
|
170
|
+
// Check for empty relation fields
|
|
171
|
+
const emptyRelations = await checkEmptyRelations(prep.tenantId, params.collectionSlug, response.data.data);
|
|
172
|
+
return formatCreatedItem(response.data, params.collectionSlug, params.projectId, emptyRelations);
|
|
97
173
|
}
|
|
98
|
-
function formatCreatedItem(item, collectionSlug) {
|
|
99
|
-
|
|
174
|
+
function formatCreatedItem(item, collectionSlug, projectId, emptyRelations) {
|
|
175
|
+
let output = `# Item Created Successfully
|
|
100
176
|
|
|
101
177
|
**Collection:** ${collectionSlug}
|
|
102
178
|
**Name:** ${item.name}
|
|
@@ -109,13 +185,49 @@ function formatCreatedItem(item, collectionSlug) {
|
|
|
109
185
|
${JSON.stringify(item.data, null, 2)}
|
|
110
186
|
\`\`\`
|
|
111
187
|
`;
|
|
188
|
+
// Add warning about empty relation fields
|
|
189
|
+
if (emptyRelations && emptyRelations.length > 0) {
|
|
190
|
+
output += `
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## ⚠️ Empty Relation Fields
|
|
194
|
+
|
|
195
|
+
The following relation fields were not set. You may want to update them:
|
|
196
|
+
|
|
197
|
+
`;
|
|
198
|
+
for (const rel of emptyRelations) {
|
|
199
|
+
output += `### ${rel.fieldName} (\`${rel.fieldSlug}\`)
|
|
200
|
+
References: **${rel.referenceCollection}**
|
|
201
|
+
|
|
202
|
+
Available options:
|
|
203
|
+
`;
|
|
204
|
+
for (const opt of rel.options.slice(0, 5)) {
|
|
205
|
+
output += `- \`"${opt.id}"\` → ${opt.name}${opt.slug ? ` (${opt.slug})` : ''}\n`;
|
|
206
|
+
}
|
|
207
|
+
if (rel.options.length > 5) {
|
|
208
|
+
output += `- ... and ${rel.options.length - 5} more\n`;
|
|
209
|
+
}
|
|
210
|
+
output += '\n';
|
|
211
|
+
}
|
|
212
|
+
output += `**To update**, use:
|
|
213
|
+
\`\`\`
|
|
214
|
+
update_cms_item(
|
|
215
|
+
projectId: "${projectId || '...'}",
|
|
216
|
+
collectionSlug: "${collectionSlug}",
|
|
217
|
+
itemSlug: "${item.slug || '...'}",
|
|
218
|
+
data: { ${emptyRelations.map(r => `${r.fieldSlug}: "${r.options[0]?.id || 'ITEM_ID'}"`).join(', ')} }
|
|
219
|
+
)
|
|
220
|
+
\`\`\`
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
return output;
|
|
112
224
|
}
|
|
113
225
|
/**
|
|
114
226
|
* List items in a CMS collection
|
|
115
227
|
*/
|
|
116
228
|
async function listCmsItems(params) {
|
|
117
229
|
const prep = await prepareRequest(params.projectId);
|
|
118
|
-
if (
|
|
230
|
+
if (prepFailed(prep))
|
|
119
231
|
return prep.message;
|
|
120
232
|
// Get collection ID from slug
|
|
121
233
|
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
|
|
@@ -189,7 +301,7 @@ Found ${items.length} item${items.length !== 1 ? 's' : ''}:
|
|
|
189
301
|
*/
|
|
190
302
|
async function getCmsItem(params) {
|
|
191
303
|
const prep = await prepareRequest(params.projectId);
|
|
192
|
-
if (
|
|
304
|
+
if (prepFailed(prep))
|
|
193
305
|
return prep.message;
|
|
194
306
|
// Get collection ID from slug
|
|
195
307
|
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
|
|
@@ -243,7 +355,7 @@ ${JSON.stringify(item.data, null, 2)}
|
|
|
243
355
|
*/
|
|
244
356
|
async function updateCmsItem(params) {
|
|
245
357
|
const prep = await prepareRequest(params.projectId);
|
|
246
|
-
if (
|
|
358
|
+
if (prepFailed(prep))
|
|
247
359
|
return prep.message;
|
|
248
360
|
// Get collection ID from slug
|
|
249
361
|
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
|
|
@@ -336,7 +448,7 @@ To delete the item "${params.itemSlug}" from collection "${params.collectionSlug
|
|
|
336
448
|
`;
|
|
337
449
|
}
|
|
338
450
|
const prep = await prepareRequest(params.projectId);
|
|
339
|
-
if (
|
|
451
|
+
if (prepFailed(prep))
|
|
340
452
|
return prep.message;
|
|
341
453
|
// Get collection ID from slug
|
|
342
454
|
const collectionResult = await getCollectionId(prep.tenantId, params.collectionSlug);
|
|
@@ -374,3 +486,86 @@ Successfully deleted item "${params.itemSlug}" from collection "${params.collect
|
|
|
374
486
|
Successfully deleted item "${params.itemSlug}" from collection "${params.collectionSlug}".
|
|
375
487
|
`;
|
|
376
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Get relation field options for a collection
|
|
491
|
+
* Shows all relation fields and their available values
|
|
492
|
+
*/
|
|
493
|
+
async function getRelationOptions(params) {
|
|
494
|
+
const prep = await prepareRequest(params.projectId);
|
|
495
|
+
if (prepFailed(prep))
|
|
496
|
+
return prep.message;
|
|
497
|
+
// Get collection with fields
|
|
498
|
+
const collResult = await getCollectionWithFields(prep.tenantId, params.collectionSlug);
|
|
499
|
+
if ('error' in collResult) {
|
|
500
|
+
return `# Error\n\n${collResult.error}`;
|
|
501
|
+
}
|
|
502
|
+
const { collection } = collResult;
|
|
503
|
+
// Find relation fields
|
|
504
|
+
const relationFields = (collection.fields || []).filter(f => f.type === 'relation' && f.referenceCollection);
|
|
505
|
+
if (relationFields.length === 0) {
|
|
506
|
+
return `# No Relation Fields
|
|
507
|
+
|
|
508
|
+
Collection **${params.collectionSlug}** has no relation fields.
|
|
509
|
+
|
|
510
|
+
Relation fields link items to other collections. To create one, use:
|
|
511
|
+
\`\`\`
|
|
512
|
+
sync_schema(
|
|
513
|
+
projectId: "${params.projectId}",
|
|
514
|
+
fieldsToAdd: [{
|
|
515
|
+
collectionSlug: "${params.collectionSlug}",
|
|
516
|
+
fields: [{ slug: "category", name: "Category", type: "relation", referenceCollection: "categories" }]
|
|
517
|
+
}]
|
|
518
|
+
)
|
|
519
|
+
\`\`\`
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
// If a specific field is requested, filter to just that one
|
|
523
|
+
const fieldsToShow = params.fieldSlug
|
|
524
|
+
? relationFields.filter(f => f.slug === params.fieldSlug)
|
|
525
|
+
: relationFields;
|
|
526
|
+
if (params.fieldSlug && fieldsToShow.length === 0) {
|
|
527
|
+
const availableFields = relationFields.map(f => f.slug).join(', ');
|
|
528
|
+
return `# Field Not Found
|
|
529
|
+
|
|
530
|
+
Field "${params.fieldSlug}" is not a relation field in collection "${params.collectionSlug}".
|
|
531
|
+
|
|
532
|
+
Available relation fields: ${availableFields || 'none'}
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
let output = `# Relation Field Options for ${collection.name}
|
|
536
|
+
|
|
537
|
+
`;
|
|
538
|
+
for (const field of fieldsToShow) {
|
|
539
|
+
const options = await getCollectionItems(prep.tenantId, field.referenceCollection, 20);
|
|
540
|
+
output += `## ${field.name} (\`${field.slug}\`)
|
|
541
|
+
**References:** ${field.referenceCollection}
|
|
542
|
+
|
|
543
|
+
`;
|
|
544
|
+
if (options.length === 0) {
|
|
545
|
+
output += `*No items in ${field.referenceCollection} yet.*
|
|
546
|
+
|
|
547
|
+
To create items, use:
|
|
548
|
+
\`\`\`
|
|
549
|
+
create_cms_item(projectId: "${params.projectId}", collectionSlug: "${field.referenceCollection}", name: "Item Name", data: {...})
|
|
550
|
+
\`\`\`
|
|
551
|
+
|
|
552
|
+
`;
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
output += `| ID | Name | Slug |
|
|
556
|
+
|----|------|------|
|
|
557
|
+
`;
|
|
558
|
+
for (const opt of options) {
|
|
559
|
+
output += `| \`${opt.id}\` | ${opt.name} | ${opt.slug || '-'} |\n`;
|
|
560
|
+
}
|
|
561
|
+
output += `
|
|
562
|
+
**Usage:** When creating/updating items in \`${params.collectionSlug}\`, use the ID:
|
|
563
|
+
\`\`\`
|
|
564
|
+
data: { ${field.slug}: "${options[0].id}" }
|
|
565
|
+
\`\`\`
|
|
566
|
+
|
|
567
|
+
`;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return output;
|
|
571
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-started.d.ts","sourceRoot":"","sources":["../../src/tools/get-started.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"get-started.d.ts","sourceRoot":"","sources":["../../src/tools/get-started.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA8CH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,eAAe,GAAG,SAAS,GAAG,QAAQ,CAAC;AAExF,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgnBD;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA4FxE"}
|
|
@@ -12,6 +12,27 @@ exports.getStarted = getStarted;
|
|
|
12
12
|
const context_fetcher_1 = require("../lib/context-fetcher");
|
|
13
13
|
const device_flow_1 = require("../lib/device-flow");
|
|
14
14
|
const api_client_1 = require("../lib/api-client");
|
|
15
|
+
/**
|
|
16
|
+
* Fetch items from a collection for relation field options
|
|
17
|
+
*/
|
|
18
|
+
async function fetchRelationOptions(tenantId, collectionSlug, limit = 5) {
|
|
19
|
+
// First get collection ID
|
|
20
|
+
const collectionsResponse = await (0, api_client_1.apiRequest)('/api/collections', { tenantId });
|
|
21
|
+
if ((0, api_client_1.isApiError)(collectionsResponse))
|
|
22
|
+
return [];
|
|
23
|
+
const collection = collectionsResponse.data.find(c => c.slug === collectionSlug);
|
|
24
|
+
if (!collection)
|
|
25
|
+
return [];
|
|
26
|
+
// Then get items
|
|
27
|
+
const itemsResponse = await (0, api_client_1.apiRequest)(`/api/collections/${collection.id}/items?limit=${limit}`, { tenantId, method: 'GET' });
|
|
28
|
+
if ((0, api_client_1.isApiError)(itemsResponse))
|
|
29
|
+
return [];
|
|
30
|
+
return itemsResponse.data.map(item => ({
|
|
31
|
+
id: item.id,
|
|
32
|
+
name: item.name,
|
|
33
|
+
slug: item.slug,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
15
36
|
// ============ Response Builders ============
|
|
16
37
|
/**
|
|
17
38
|
* Build the explore response - overview of all projects
|
|
@@ -95,7 +116,7 @@ get_started(intent: "convert")
|
|
|
95
116
|
/**
|
|
96
117
|
* Build the add_content response with schema details
|
|
97
118
|
*/
|
|
98
|
-
function buildAddContentResponse(project) {
|
|
119
|
+
async function buildAddContentResponse(project) {
|
|
99
120
|
let output = `# Fast Mode MCP - Add Content
|
|
100
121
|
|
|
101
122
|
## Project: ${project.name}
|
|
@@ -116,8 +137,61 @@ get_started(intent: "update_schema", projectId: "${project.id}")
|
|
|
116
137
|
output += `## Collections
|
|
117
138
|
|
|
118
139
|
`;
|
|
140
|
+
// Collect all relation fields across collections
|
|
141
|
+
const allRelationFields = [];
|
|
119
142
|
for (const collection of project.collections) {
|
|
120
143
|
output += buildCollectionTable(collection);
|
|
144
|
+
// Track relation fields
|
|
145
|
+
for (const field of collection.fields) {
|
|
146
|
+
if (field.type === 'relation' && field.referenceCollection) {
|
|
147
|
+
allRelationFields.push({ collection: collection.slug, field });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// If there are relation fields, show their options
|
|
152
|
+
if (allRelationFields.length > 0) {
|
|
153
|
+
output += `---
|
|
154
|
+
|
|
155
|
+
## 📋 Relation Field Options
|
|
156
|
+
|
|
157
|
+
**Important:** Relation fields require item IDs, not names. Here are the available options:
|
|
158
|
+
|
|
159
|
+
`;
|
|
160
|
+
// Group by referenced collection to avoid duplicate fetches
|
|
161
|
+
const byRefCollection = new Map();
|
|
162
|
+
for (const rf of allRelationFields) {
|
|
163
|
+
const key = rf.field.referenceCollection;
|
|
164
|
+
if (!byRefCollection.has(key)) {
|
|
165
|
+
byRefCollection.set(key, []);
|
|
166
|
+
}
|
|
167
|
+
byRefCollection.get(key).push(rf);
|
|
168
|
+
}
|
|
169
|
+
// Fetch options for each referenced collection
|
|
170
|
+
for (const [refCollection, fields] of byRefCollection) {
|
|
171
|
+
const options = await fetchRelationOptions(project.id, refCollection, 5);
|
|
172
|
+
const fieldNames = fields.map(f => `\`${f.field.slug}\` in ${f.collection}`).join(', ');
|
|
173
|
+
output += `### ${refCollection}
|
|
174
|
+
Used by: ${fieldNames}
|
|
175
|
+
|
|
176
|
+
`;
|
|
177
|
+
if (options.length === 0) {
|
|
178
|
+
output += `*No items yet.* Create some first:
|
|
179
|
+
\`\`\`
|
|
180
|
+
create_cms_item(projectId: "${project.id}", collectionSlug: "${refCollection}", name: "Item Name", data: {...})
|
|
181
|
+
\`\`\`
|
|
182
|
+
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
for (const opt of options) {
|
|
187
|
+
output += `- \`"${opt.id}"\` → **${opt.name}**${opt.slug ? ` (${opt.slug})` : ''}\n`;
|
|
188
|
+
}
|
|
189
|
+
if (options.length === 5) {
|
|
190
|
+
output += `- *... more available via* \`list_cms_items(projectId: "${project.id}", collectionSlug: "${refCollection}")\`\n`;
|
|
191
|
+
}
|
|
192
|
+
output += '\n';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
121
195
|
}
|
|
122
196
|
output += `---
|
|
123
197
|
|
|
@@ -169,12 +243,14 @@ data: { author: "John Smith" }
|
|
|
169
243
|
// CORRECT:
|
|
170
244
|
data: { author: "abc-123-uuid-of-john" }
|
|
171
245
|
\`\`\`
|
|
172
|
-
|
|
246
|
+
${allRelationFields.length > 0 ? `
|
|
247
|
+
**See "Relation Field Options" above for available IDs.**
|
|
248
|
+
` : `
|
|
173
249
|
To get the ID, first list items in the related collection:
|
|
174
250
|
\`\`\`
|
|
175
251
|
list_cms_items(projectId: "${project.id}", collectionSlug: "authors")
|
|
176
252
|
\`\`\`
|
|
177
|
-
|
|
253
|
+
`}
|
|
178
254
|
### Delete requires confirmation
|
|
179
255
|
Always ask the user before deleting:
|
|
180
256
|
\`\`\`
|
|
@@ -572,7 +648,7 @@ Could not find project: "${projectId}"
|
|
|
572
648
|
|
|
573
649
|
Use \`list_projects\` to see available projects.`;
|
|
574
650
|
}
|
|
575
|
-
return buildAddContentResponse(context.selectedProject);
|
|
651
|
+
return await buildAddContentResponse(context.selectedProject);
|
|
576
652
|
case 'update_schema':
|
|
577
653
|
if (!context.selectedProject) {
|
|
578
654
|
if (!projectId) {
|
|
@@ -617,7 +693,7 @@ Use \`list_projects\` to see available projects.`;
|
|
|
617
693
|
default:
|
|
618
694
|
// If projectId provided, show detailed project info
|
|
619
695
|
if (context.selectedProject) {
|
|
620
|
-
return buildAddContentResponse(context.selectedProject);
|
|
696
|
+
return await buildAddContentResponse(context.selectedProject);
|
|
621
697
|
}
|
|
622
698
|
return buildExploreResponse(context);
|
|
623
699
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fastmode-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MCP server for FastMode CMS. Convert websites, validate packages, and deploy directly to FastMode. Includes authentication, project creation, schema sync, and one-click deployment.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "NODE_OPTIONS='--max-old-space-size=4096' tsc --skipLibCheck && chmod +x dist/index.js",
|
|
12
12
|
"dev": "ts-node src/index.ts",
|
|
13
|
-
"start": "node dist/index.js"
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"mcp",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@types/adm-zip": "^0.5.7",
|
|
49
50
|
"@types/node": "^20.11.0",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
51
|
+
"typescript": "^5.3.3",
|
|
52
|
+
"ts-node": "^10.9.2"
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall script that downloads the correct prebuilt binary for the current platform.
|
|
4
|
+
* This allows the MCP server to run without Node.js in the PATH.
|
|
5
|
+
*/
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAME = 'fastmode-mcp';
|
|
11
|
+
const BINARY_NAME = 'fastmode-mcp';
|
|
12
|
+
|
|
13
|
+
// Get package version from package.json
|
|
14
|
+
const packageJson = require('../package.json');
|
|
15
|
+
const VERSION = packageJson.version;
|
|
16
|
+
|
|
17
|
+
// Platform/arch mapping
|
|
18
|
+
const PLATFORM_MAP = {
|
|
19
|
+
'darwin-arm64': 'macos-arm64',
|
|
20
|
+
'darwin-x64': 'macos-x64',
|
|
21
|
+
'linux-arm64': 'linux-arm64',
|
|
22
|
+
'linux-x64': 'linux-x64',
|
|
23
|
+
'win32-x64': 'win-x64',
|
|
24
|
+
'win32-arm64': 'win-arm64',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function getPlatformKey() {
|
|
28
|
+
const platform = process.platform;
|
|
29
|
+
const arch = process.arch;
|
|
30
|
+
return `${platform}-${arch}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getBinaryUrl(version, platformKey) {
|
|
34
|
+
const platformSuffix = PLATFORM_MAP[platformKey];
|
|
35
|
+
if (!platformSuffix) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
40
|
+
const filename = `${BINARY_NAME}-${platformSuffix}${ext}`;
|
|
41
|
+
|
|
42
|
+
// Download from GitHub Releases (public repo for binaries)
|
|
43
|
+
return `https://github.com/arihgoldstein/fastmode-mcp/releases/download/v${version}/${filename}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function downloadFile(url, destPath) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const file = fs.createWriteStream(destPath);
|
|
49
|
+
|
|
50
|
+
const request = (url) => {
|
|
51
|
+
https.get(url, (response) => {
|
|
52
|
+
// Handle redirects
|
|
53
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
54
|
+
request(response.headers.location);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (response.statusCode === 404) {
|
|
59
|
+
file.close();
|
|
60
|
+
fs.unlinkSync(destPath);
|
|
61
|
+
reject(new Error(`Binary not found at ${url}`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (response.statusCode !== 200) {
|
|
66
|
+
file.close();
|
|
67
|
+
fs.unlinkSync(destPath);
|
|
68
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
response.pipe(file);
|
|
73
|
+
file.on('finish', () => {
|
|
74
|
+
file.close();
|
|
75
|
+
resolve();
|
|
76
|
+
});
|
|
77
|
+
}).on('error', (err) => {
|
|
78
|
+
file.close();
|
|
79
|
+
fs.unlinkSync(destPath);
|
|
80
|
+
reject(err);
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
request(url);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function main() {
|
|
89
|
+
const platformKey = getPlatformKey();
|
|
90
|
+
const binaryUrl = getBinaryUrl(VERSION, platformKey);
|
|
91
|
+
|
|
92
|
+
if (!binaryUrl) {
|
|
93
|
+
console.log(`No prebuilt binary available for ${platformKey}`);
|
|
94
|
+
console.log('Falling back to Node.js execution');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const binDir = path.join(__dirname, '..', 'bin');
|
|
99
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
100
|
+
const binaryPath = path.join(binDir, `${BINARY_NAME}${ext}`);
|
|
101
|
+
|
|
102
|
+
// Create bin directory if it doesn't exist
|
|
103
|
+
if (!fs.existsSync(binDir)) {
|
|
104
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`Downloading ${PACKAGE_NAME} binary for ${platformKey}...`);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await downloadFile(binaryUrl, binaryPath);
|
|
111
|
+
|
|
112
|
+
// Make executable on Unix
|
|
113
|
+
if (process.platform !== 'win32') {
|
|
114
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`Successfully installed ${BINARY_NAME} binary`);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Binary download failed - this is OK, we'll fall back to Node.js
|
|
120
|
+
console.log(`Binary download failed: ${error.message}`);
|
|
121
|
+
console.log('Falling back to Node.js execution');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main().catch((error) => {
|
|
126
|
+
// Don't fail the install if binary download fails
|
|
127
|
+
console.error('Postinstall warning:', error.message);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get-schema.d.ts","sourceRoot":"","sources":["../../src/tools/get-schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAyTjD"}
|
package/dist/tools/get-schema.js
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getSchema = getSchema;
|
|
4
|
-
/**
|
|
5
|
-
* Returns the complete CMS schema reference for custom collections
|
|
6
|
-
*/
|
|
7
|
-
async function getSchema() {
|
|
8
|
-
return `# CMS Schema Reference
|
|
9
|
-
|
|
10
|
-
## Collections Overview
|
|
11
|
-
|
|
12
|
-
All CMS content is managed through **custom collections**. Collections are content types you define (e.g., Blog Posts, Team Members, Services, Products) with custom fields.
|
|
13
|
-
|
|
14
|
-
**To see the exact collections and fields for a specific project, use the \`get_tenant_schema\` tool.**
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## Collection Templates
|
|
19
|
-
|
|
20
|
-
When creating a new collection, you can start from a **template** that pre-configures common field sets:
|
|
21
|
-
|
|
22
|
-
- **Blog Posts** - Articles with images, summary, body, author reference
|
|
23
|
-
- **Authors** - Content creators with bio, photo, social links
|
|
24
|
-
- **Team Members** - Staff profiles with role, photo, bio
|
|
25
|
-
- **Downloads** - Downloadable files with description and category
|
|
26
|
-
- **FAQs** - Question and answer pairs
|
|
27
|
-
- **Products** - Items with price, description, images
|
|
28
|
-
|
|
29
|
-
Templates are just starting points - you can customize fields after creation.
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Token Syntax
|
|
34
|
-
|
|
35
|
-
### Basic Tokens
|
|
36
|
-
\`\`\`html
|
|
37
|
-
{{fieldSlug}} <!-- For text, number, boolean, image, url, date, select -->
|
|
38
|
-
{{{fieldSlug}}} <!-- For richText fields (MUST use triple braces) -->
|
|
39
|
-
\`\`\`
|
|
40
|
-
|
|
41
|
-
### Built-in Tokens (Available on ALL items)
|
|
42
|
-
- \`{{name}}\` - Item name/title (REQUIRED - every item has a name)
|
|
43
|
-
- \`{{slug}}\` - Item URL slug (if collection has detail pages)
|
|
44
|
-
- \`{{url}}\` - Full item URL (e.g., /services/my-service)
|
|
45
|
-
|
|
46
|
-
### Date Tokens (Automatically tracked)
|
|
47
|
-
- \`{{createdAt}}\` - When the item was first created
|
|
48
|
-
- \`{{publishedAt}}\` - When the item was published (empty for drafts)
|
|
49
|
-
- \`{{updatedAt}}\` - When the item was last updated
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## Loop Syntax
|
|
54
|
-
|
|
55
|
-
### Basic Loop
|
|
56
|
-
\`\`\`html
|
|
57
|
-
{{#each collectionSlug}}
|
|
58
|
-
<article>
|
|
59
|
-
<h2>{{name}}</h2>
|
|
60
|
-
</article>
|
|
61
|
-
{{/each}}
|
|
62
|
-
\`\`\`
|
|
63
|
-
|
|
64
|
-
### Loop Modifiers
|
|
65
|
-
- \`limit=N\` - Limit to N items
|
|
66
|
-
- \`featured=true\` - Only featured items (if collection has a boolean "featured" field)
|
|
67
|
-
- \`sort="fieldName"\` - Sort by field
|
|
68
|
-
- \`order="asc|desc"\` - Sort direction
|
|
69
|
-
|
|
70
|
-
\`\`\`html
|
|
71
|
-
{{#each posts limit=6 sort="publishedAt" order="desc"}}
|
|
72
|
-
<article>{{name}}</article>
|
|
73
|
-
{{/each}}
|
|
74
|
-
\`\`\`
|
|
75
|
-
|
|
76
|
-
### Loop Variables
|
|
77
|
-
Inside \`{{#each}}\` blocks:
|
|
78
|
-
- \`{{@first}}\` - true for first item
|
|
79
|
-
- \`{{@last}}\` - true for last item
|
|
80
|
-
- \`{{@index}}\` - zero-based index
|
|
81
|
-
|
|
82
|
-
\`\`\`html
|
|
83
|
-
{{#each services limit=3}}
|
|
84
|
-
<div class="{{#if @first}}featured{{/if}}">
|
|
85
|
-
{{name}}
|
|
86
|
-
</div>
|
|
87
|
-
{{/each}}
|
|
88
|
-
\`\`\`
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Conditionals
|
|
93
|
-
|
|
94
|
-
### Check if field has value
|
|
95
|
-
\`\`\`html
|
|
96
|
-
{{#if image}}
|
|
97
|
-
<img src="{{image}}" alt="{{name}}">
|
|
98
|
-
{{else}}
|
|
99
|
-
<div class="placeholder"></div>
|
|
100
|
-
{{/if}}
|
|
101
|
-
|
|
102
|
-
{{#unless featured}}
|
|
103
|
-
<span>Regular item</span>
|
|
104
|
-
{{/unless}}
|
|
105
|
-
\`\`\`
|
|
106
|
-
|
|
107
|
-
### Collection Empty Checks
|
|
108
|
-
\`\`\`html
|
|
109
|
-
{{#each posts}}
|
|
110
|
-
<article>{{name}}</article>
|
|
111
|
-
{{/each}}
|
|
112
|
-
|
|
113
|
-
{{#unless posts}}
|
|
114
|
-
<p>No posts yet. Check back soon!</p>
|
|
115
|
-
{{/unless}}
|
|
116
|
-
\`\`\`
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## Equality Comparisons
|
|
121
|
-
|
|
122
|
-
Compare field values using the \`(eq field1 field2)\` helper:
|
|
123
|
-
|
|
124
|
-
\`\`\`html
|
|
125
|
-
<!-- Show content when fields ARE equal -->
|
|
126
|
-
{{#if (eq category.slug ../slug)}}
|
|
127
|
-
<span>Current category</span>
|
|
128
|
-
{{/if}}
|
|
129
|
-
|
|
130
|
-
<!-- Show content when fields are NOT equal (exclude current) -->
|
|
131
|
-
{{#unless (eq slug ../slug)}}
|
|
132
|
-
<a href="{{url}}">{{name}}</a>
|
|
133
|
-
{{/unless}}
|
|
134
|
-
\`\`\`
|
|
135
|
-
|
|
136
|
-
### Related Items Pattern (Exclude Current)
|
|
137
|
-
\`\`\`html
|
|
138
|
-
<h3>Other Posts</h3>
|
|
139
|
-
{{#each posts limit=3}}
|
|
140
|
-
{{#unless (eq slug ../slug)}}
|
|
141
|
-
<article>
|
|
142
|
-
<a href="{{url}}">{{name}}</a>
|
|
143
|
-
</article>
|
|
144
|
-
{{/unless}}
|
|
145
|
-
{{/each}}
|
|
146
|
-
\`\`\`
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
## Parent Context References
|
|
151
|
-
|
|
152
|
-
Inside loops, access the parent scope using \`../\`:
|
|
153
|
-
|
|
154
|
-
\`\`\`html
|
|
155
|
-
<!-- On author detail page, show only posts by THIS author -->
|
|
156
|
-
{{#each posts}}
|
|
157
|
-
{{#if (eq author.name ../name)}}
|
|
158
|
-
<h3>{{name}}</h3>
|
|
159
|
-
{{/if}}
|
|
160
|
-
{{/each}}
|
|
161
|
-
\`\`\`
|
|
162
|
-
|
|
163
|
-
- \`../name\` - Parent item's name field
|
|
164
|
-
- \`../slug\` - Parent item's slug
|
|
165
|
-
- \`../fieldName\` - Any field from the parent scope
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
## Relation Fields
|
|
170
|
-
|
|
171
|
-
Link items from one collection to another. Access related item data using dot notation:
|
|
172
|
-
|
|
173
|
-
\`\`\`html
|
|
174
|
-
{{#each projects}}
|
|
175
|
-
<article>
|
|
176
|
-
<h2>{{name}}</h2>
|
|
177
|
-
{{#if category}}
|
|
178
|
-
<span class="tag">{{category.name}}</span>
|
|
179
|
-
<a href="{{category.url}}">View all {{category.name}}</a>
|
|
180
|
-
{{/if}}
|
|
181
|
-
</article>
|
|
182
|
-
{{/each}}
|
|
183
|
-
\`\`\`
|
|
184
|
-
|
|
185
|
-
**Available tokens for related items:**
|
|
186
|
-
- \`{{relationField.name}}\` - Related item's name
|
|
187
|
-
- \`{{relationField.slug}}\` - Related item's slug
|
|
188
|
-
- \`{{relationField.url}}\` - Related item's full URL
|
|
189
|
-
- \`{{relationField.anyField}}\` - Any field from the related collection
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
## Rich Text (Triple Braces)
|
|
194
|
-
|
|
195
|
-
For HTML content that should NOT be escaped:
|
|
196
|
-
\`\`\`html
|
|
197
|
-
{{{description}}} ✓ Correct - renders HTML
|
|
198
|
-
{{description}} ✗ Wrong - escapes HTML as text
|
|
199
|
-
\`\`\`
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
## Important Rules
|
|
204
|
-
|
|
205
|
-
1. **Triple braces for richText fields** - \`{{{body}}}\`, \`{{{bio}}}\`
|
|
206
|
-
2. **Double braces for everything else** - \`{{name}}\`, \`{{image}}\`
|
|
207
|
-
3. **Always wrap optional fields in {{#if}}** - Check before rendering
|
|
208
|
-
4. **Use {{url}} for links** - Generates correct path based on manifest
|
|
209
|
-
5. **Match field slugs exactly** - Case-sensitive
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## Image Handling
|
|
214
|
-
|
|
215
|
-
### Static UI Images (logos, icons)
|
|
216
|
-
\`\`\`html
|
|
217
|
-
<img src="/public/images/logo.png" alt="Logo">
|
|
218
|
-
\`\`\`
|
|
219
|
-
|
|
220
|
-
### CMS Content Images
|
|
221
|
-
\`\`\`html
|
|
222
|
-
{{#if heroImage}}
|
|
223
|
-
<img src="{{heroImage}}" alt="{{name}}">
|
|
224
|
-
{{/if}}
|
|
225
|
-
\`\`\`
|
|
226
|
-
|
|
227
|
-
---
|
|
228
|
-
|
|
229
|
-
## CMS Template Configuration
|
|
230
|
-
|
|
231
|
-
Configure templates in manifest.json:
|
|
232
|
-
|
|
233
|
-
\`\`\`json
|
|
234
|
-
{
|
|
235
|
-
"cmsTemplates": {
|
|
236
|
-
"postsIndex": "pages/blog.html",
|
|
237
|
-
"postsIndexPath": "/blog",
|
|
238
|
-
"postsDetail": "templates/blog-post.html",
|
|
239
|
-
"postsDetailPath": "/blog",
|
|
240
|
-
|
|
241
|
-
"servicesIndex": "pages/services.html",
|
|
242
|
-
"servicesIndexPath": "/services",
|
|
243
|
-
"servicesDetail": "templates/service-detail.html",
|
|
244
|
-
"servicesDetailPath": "/services"
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
\`\`\`
|
|
248
|
-
|
|
249
|
-
### Template Keys Pattern
|
|
250
|
-
- **{collectionSlug}Index** - Template file for listing page
|
|
251
|
-
- **{collectionSlug}IndexPath** - URL path for listing page
|
|
252
|
-
- **{collectionSlug}Detail** - Template file for detail page
|
|
253
|
-
- **{collectionSlug}DetailPath** - URL base for detail pages
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
## Form Handling
|
|
258
|
-
|
|
259
|
-
Forms are automatically captured and stored in the CMS.
|
|
260
|
-
|
|
261
|
-
\`\`\`html
|
|
262
|
-
<form data-form-name="contact">
|
|
263
|
-
<input type="text" name="firstName" required>
|
|
264
|
-
<input type="email" name="email" required>
|
|
265
|
-
<textarea name="message"></textarea>
|
|
266
|
-
<button type="submit">Send</button>
|
|
267
|
-
</form>
|
|
268
|
-
\`\`\`
|
|
269
|
-
|
|
270
|
-
### Form Handler Script
|
|
271
|
-
\`\`\`javascript
|
|
272
|
-
document.querySelectorAll('form[data-form-name]').forEach(form => {
|
|
273
|
-
form.addEventListener('submit', async (e) => {
|
|
274
|
-
e.preventDefault();
|
|
275
|
-
const formName = form.dataset.formName || 'general';
|
|
276
|
-
const formData = new FormData(form);
|
|
277
|
-
const data = Object.fromEntries(formData);
|
|
278
|
-
|
|
279
|
-
const response = await fetch('/_forms/' + formName, {
|
|
280
|
-
method: 'POST',
|
|
281
|
-
headers: { 'Content-Type': 'application/json' },
|
|
282
|
-
body: JSON.stringify(data)
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
if (response.ok) {
|
|
286
|
-
form.reset();
|
|
287
|
-
alert(form.dataset.successMessage || 'Thank you!');
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
\`\`\`
|
|
292
|
-
|
|
293
|
-
**CRITICAL:** Endpoint is \`/_forms/{formName}\` - NOT \`/api/forms/submit\`
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
## SEO Template Tokens
|
|
298
|
-
|
|
299
|
-
For CMS detail pages, use tokens in SEO templates:
|
|
300
|
-
- \`{{name}}\` - Item name for title
|
|
301
|
-
- \`{{description}}\` or summary field - For meta description
|
|
302
|
-
- \`{{image}}\` field - For OG image
|
|
303
|
-
|
|
304
|
-
SEO templates are configured in the Fast Mode Editor.
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## Next Steps
|
|
309
|
-
|
|
310
|
-
**To see exact collections and fields for a specific project, use:**
|
|
311
|
-
\`\`\`
|
|
312
|
-
get_tenant_schema(projectId: "your-project-name-or-id")
|
|
313
|
-
\`\`\`
|
|
314
|
-
|
|
315
|
-
This will show you:
|
|
316
|
-
- All collections with their slugs
|
|
317
|
-
- All fields with their tokens, types, and descriptions
|
|
318
|
-
- Relation field targets
|
|
319
|
-
`;
|
|
320
|
-
}
|