multisite-cms-mcp 1.2.2 → 1.2.4

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
@@ -127,6 +127,7 @@ const TOOLS = [
127
127
  'featured_posts',
128
128
  'parent_context',
129
129
  'equality_comparison',
130
+ 'youtube_embed',
130
131
  ],
131
132
  description: 'The type of example to retrieve',
132
133
  },
@@ -192,7 +193,7 @@ const TOOLS = [
192
193
  },
193
194
  {
194
195
  name: 'deploy_package',
195
- description: 'Deploy a website package (.zip file) to Fast Mode. Can deploy to an existing project or create a new one. Opens browser for authentication if not already logged in. Use list_projects first to see existing projects.',
196
+ description: 'Deploy a website package (.zip file) to Fast Mode. Will check for GitHub sync and block deployment if connected (to prevent conflicts). Use force:true to override. Opens browser for authentication if not already logged in.',
196
197
  inputSchema: {
197
198
  type: 'object',
198
199
  properties: {
@@ -208,6 +209,10 @@ const TOOLS = [
208
209
  type: 'string',
209
210
  description: 'Optional: Create a new project with this name and deploy to it. Required if projectId is not provided and no projects exist.',
210
211
  },
212
+ force: {
213
+ type: 'boolean',
214
+ description: 'Optional: Skip GitHub connection check and deploy anyway. Use with caution - may cause conflicts if GitHub auto-deploy is enabled.',
215
+ },
211
216
  },
212
217
  required: ['packagePath'],
213
218
  },
@@ -345,7 +350,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
345
350
  result = await (0, create_site_1.createSite)(params.name, params.subdomain);
346
351
  break;
347
352
  case 'deploy_package':
348
- result = await (0, deploy_package_1.deployPackage)(params.packagePath, params.projectId, params.projectName);
353
+ result = await (0, deploy_package_1.deployPackage)(params.packagePath, params.projectId, params.projectName, params.force);
349
354
  break;
350
355
  case 'get_field_types':
351
356
  result = await (0, get_field_types_1.getFieldTypes)();
@@ -4,6 +4,7 @@
4
4
  * @param packagePath - Path to the zip file
5
5
  * @param projectId - Optional: Deploy to existing project with this ID
6
6
  * @param projectName - Optional: Create new project with this name
7
+ * @param force - Optional: Skip GitHub connection check and deploy anyway
7
8
  */
8
- export declare function deployPackage(packagePath: string, projectId?: string, projectName?: string): Promise<string>;
9
+ export declare function deployPackage(packagePath: string, projectId?: string, projectName?: string, force?: boolean): Promise<string>;
9
10
  //# sourceMappingURL=deploy-package.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deploy-package.d.ts","sourceRoot":"","sources":["../../src/tools/deploy-package.ts"],"names":[],"mappings":"AA+MA;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CA0IjB"}
1
+ {"version":3,"file":"deploy-package.d.ts","sourceRoot":"","sources":["../../src/tools/deploy-package.ts"],"names":[],"mappings":"AAmRA;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,OAAO,GACd,OAAO,CAAC,MAAM,CAAC,CAwJjB"}
@@ -49,6 +49,60 @@ async function listExistingProjects() {
49
49
  }
50
50
  return response.data;
51
51
  }
52
+ /**
53
+ * Check if a project has GitHub connected
54
+ */
55
+ async function checkGitHubConnection(tenantId) {
56
+ const response = await (0, api_client_1.apiRequest)('/api/github/site-connection', { tenantId });
57
+ if ((0, api_client_1.isApiError)(response)) {
58
+ // If we can't check, assume not connected (fail open for better UX)
59
+ return null;
60
+ }
61
+ return response.data;
62
+ }
63
+ /**
64
+ * Format the GitHub blocking message
65
+ */
66
+ function formatGitHubBlockMessage(repo, branch, subdomain) {
67
+ return `# ⚠️ GitHub Sync Detected
68
+
69
+ This project has GitHub connected and will auto-deploy from:
70
+ - **Repository:** ${repo}
71
+ - **Branch:** ${branch}
72
+
73
+ ## Why This Matters
74
+
75
+ Deploying via MCP while GitHub is connected can cause conflicts:
76
+ - Your MCP changes could be overwritten on the next GitHub push
77
+ - Or this deploy could overwrite changes from GitHub
78
+
79
+ ## Options
80
+
81
+ ### Option 1: Push to GitHub Instead (Recommended)
82
+ Push your changes to the connected repository and let GitHub handle the deploy:
83
+ \`\`\`bash
84
+ git push origin ${branch}
85
+ \`\`\`
86
+
87
+ ### Option 2: Disconnect GitHub First
88
+ Go to **app.fastmode.ai** → Settings → GitHub and disconnect the repository.
89
+
90
+ ### Option 3: Force Deploy Anyway
91
+ If you understand the risks and want to proceed:
92
+ \`\`\`
93
+ deploy_package(
94
+ packagePath: "./your-site.zip",
95
+ projectId: "...",
96
+ force: true
97
+ )
98
+ \`\`\`
99
+
100
+ ---
101
+
102
+ **Dashboard:** https://app.fastmode.ai
103
+ **Site:** https://${subdomain}.fastmode.ai
104
+ `;
105
+ }
52
106
  /**
53
107
  * Create a new site/project
54
108
  */
@@ -176,8 +230,9 @@ deploy_package(
176
230
  * @param packagePath - Path to the zip file
177
231
  * @param projectId - Optional: Deploy to existing project with this ID
178
232
  * @param projectName - Optional: Create new project with this name
233
+ * @param force - Optional: Skip GitHub connection check and deploy anyway
179
234
  */
180
- async function deployPackage(packagePath, projectId, projectName) {
235
+ async function deployPackage(packagePath, projectId, projectName, force) {
181
236
  // Check authentication
182
237
  if (await (0, api_client_1.needsAuthentication)()) {
183
238
  const authResult = await (0, device_flow_1.ensureAuthenticated)();
@@ -263,6 +318,14 @@ Please try again or create the project manually at app.fastmode.ai
263
318
  const project = projects.find(p => p.id === targetProjectId);
264
319
  subdomain = project?.subdomain || '';
265
320
  }
321
+ // Check for GitHub connection (unless force is true or it's a new project)
322
+ if (!force && !projectName && targetProjectId) {
323
+ const githubStatus = await checkGitHubConnection(targetProjectId);
324
+ if (githubStatus?.connected && githubStatus.repo) {
325
+ // GitHub is connected - block deployment
326
+ return formatGitHubBlockMessage(githubStatus.repo, githubStatus.branch || 'main', subdomain);
327
+ }
328
+ }
266
329
  // Upload the package
267
330
  console.error(`Deploying to ${subdomain}.fastmode.ai...`);
268
331
  const uploadResult = await uploadPackage(targetProjectId, zipBuffer);
@@ -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';
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';
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,CAAC;AA0pC1B;;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,CAAC;AAsvCpB;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
@@ -1155,6 +1155,97 @@ The most common pattern for "Related Posts" or "Other Items" sections:
1155
1155
  - Filter by current author/category - use \`{{#if (eq author.slug ../slug)}}\`
1156
1156
  - Highlight active items - use \`{{#if (eq ...)}}\`
1157
1157
  - Show badges for specific statuses - use \`{{#eq status "value"}}\``,
1158
+ youtube_embed: `# YouTube Video Embeds
1159
+
1160
+ **IMPORTANT:** YouTube iframes require specific attributes to work correctly. Missing attributes will cause Error 150/153 "Video player configuration error".
1161
+
1162
+ ## Correct YouTube Embed Format
1163
+
1164
+ \`\`\`html
1165
+ <iframe
1166
+ src="{{videoUrl}}"
1167
+ title="{{name}}"
1168
+ frameborder="0"
1169
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
1170
+ referrerpolicy="strict-origin-when-cross-origin"
1171
+ allowfullscreen
1172
+ ></iframe>
1173
+ \`\`\`
1174
+
1175
+ ## Required Attributes
1176
+
1177
+ | Attribute | Required | Why |
1178
+ |-----------|----------|-----|
1179
+ | \`referrerpolicy="strict-origin-when-cross-origin"\` | **YES** | YouTube blocks embeds without this |
1180
+ | \`allowfullscreen\` | Recommended | Enables fullscreen button |
1181
+ | \`allow="..."\` | Recommended | Enables player features |
1182
+ | \`frameborder="0"\` | Recommended | Removes border |
1183
+ | \`title="..."\` | Recommended | Accessibility for screen readers |
1184
+
1185
+ ## Common Mistake (Causes Error 153)
1186
+
1187
+ \`\`\`html
1188
+ <!-- ❌ WRONG - Missing referrerpolicy -->
1189
+ <iframe src="{{videoUrl}}" allowfullscreen></iframe>
1190
+
1191
+ <!-- ✅ CORRECT - All required attributes -->
1192
+ <iframe
1193
+ src="{{videoUrl}}"
1194
+ referrerpolicy="strict-origin-when-cross-origin"
1195
+ allowfullscreen
1196
+ ></iframe>
1197
+ \`\`\`
1198
+
1199
+ ## Video URL Format
1200
+
1201
+ Store YouTube URLs in embed format:
1202
+ \`\`\`
1203
+ https://www.youtube.com/embed/VIDEO_ID
1204
+ \`\`\`
1205
+
1206
+ **NOT** watch format:
1207
+ \`\`\`
1208
+ https://www.youtube.com/watch?v=VIDEO_ID ❌
1209
+ \`\`\`
1210
+
1211
+ ## Full Example with Conditional
1212
+
1213
+ \`\`\`html
1214
+ <div class="video-container">
1215
+ {{#if videoUrl}}
1216
+ <iframe
1217
+ src="{{videoUrl}}"
1218
+ title="{{name}}"
1219
+ frameborder="0"
1220
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
1221
+ referrerpolicy="strict-origin-when-cross-origin"
1222
+ allowfullscreen
1223
+ ></iframe>
1224
+ {{else}}
1225
+ <div class="video-placeholder">
1226
+ <p>Video coming soon</p>
1227
+ </div>
1228
+ {{/if}}
1229
+ </div>
1230
+ \`\`\`
1231
+
1232
+ ## Responsive Video CSS
1233
+
1234
+ \`\`\`css
1235
+ .video-container {
1236
+ position: relative;
1237
+ width: 100%;
1238
+ padding-bottom: 56.25%; /* 16:9 aspect ratio */
1239
+ }
1240
+
1241
+ .video-container iframe {
1242
+ position: absolute;
1243
+ top: 0;
1244
+ left: 0;
1245
+ width: 100%;
1246
+ height: 100%;
1247
+ }
1248
+ \`\`\``,
1158
1249
  };
1159
1250
  /**
1160
1251
  * 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,YAAY,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,eAAe,GAAG,eAAe,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;AA6O7J;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,YAAY,EAC1B,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CA4ZjB"}
1
+ {"version":3,"file":"validate-template.d.ts","sourceRoot":"","sources":["../../src/tools/validate-template.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG,YAAY,GAAG,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,eAAe,GAAG,eAAe,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;AA6O7J;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,YAAY,EAC1B,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAubjB"}
@@ -302,6 +302,28 @@ async function validateTemplate(html, templateType, collectionSlug, projectId) {
302
302
  if (wrongAssetPaths.length > 5) {
303
303
  warnings.push(`- ...and ${wrongAssetPaths.length - 5} more asset paths that may need /public/ prefix`);
304
304
  }
305
+ // Check YouTube iframes for required attributes
306
+ // Match all iframes - we'll check if they're video embeds
307
+ const allIframes = html.match(/<iframe[^>]*>/gi) || [];
308
+ for (const iframe of allIframes) {
309
+ // Check if it's likely a video embed (YouTube URL or template token in src)
310
+ const isYouTubeEmbed = /youtube\.com|youtu\.be/i.test(iframe);
311
+ const hasTemplateSrc = /src=["'][^"']*\{\{[^}]+\}\}[^"']*["']/i.test(iframe);
312
+ if (isYouTubeEmbed || hasTemplateSrc) {
313
+ // Check for referrerpolicy
314
+ if (!/referrerpolicy/i.test(iframe)) {
315
+ errors.push(`- YouTube iframe missing referrerpolicy attribute. Add: referrerpolicy="strict-origin-when-cross-origin" (without this, YouTube will show Error 153)`);
316
+ }
317
+ // Check for allowfullscreen
318
+ if (!/allowfullscreen/i.test(iframe)) {
319
+ warnings.push(`- iframe missing allowfullscreen attribute - fullscreen button won't work`);
320
+ }
321
+ // Check for title (accessibility)
322
+ if (!/title=/i.test(iframe)) {
323
+ suggestions.push(`- Consider adding a title attribute to iframe for accessibility`);
324
+ }
325
+ }
326
+ }
305
327
  // Validate site tokens
306
328
  const siteTokens = html.match(/\{\{site\.(\w+)\}\}/g) || [];
307
329
  for (const token of siteTokens) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multisite-cms-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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": {