@qikdev/mcp 6.8.0 → 6.8.3

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.
@@ -1,425 +1,1384 @@
1
- /**
2
- * Interface Builder - Main Orchestration Tool
3
- * Builds complete interfaces from natural language prompts
4
- */
5
1
  import { ConfigManager } from "../config.js";
6
- import { parseInterfacePrompt } from "./nlp-parser.js";
7
- import { getMarketplaceComponents } from "./component-marketplace.js";
8
- import { generateRoutes, generateMenus, addLegalPages } from "./route-generator.js";
9
- import { handleCreateContent } from "./create.js";
10
- /**
11
- * Build Interface from Prompt Tool
12
- */
13
- export const buildInterfaceFromPromptTool = {
14
- name: "build_interface_from_prompt",
15
- description: "🎨 Build a complete, beautiful interface from a natural language description. Examples: 'Create a portfolio website with about, work, and contact pages' or 'Build a restaurant app with menu, locations, and reservations'",
2
+ import { parseNestedJsonStrings, createErrorResponse } from "./utils.js";
3
+ import { getUserSessionData, getAvailableScopes, getScopeTitle, } from "./user.js";
4
+ // ============================================================================
5
+ // CONSTANTS
6
+ // ============================================================================
7
+ const WELL_KNOWN_COMPONENTS = {
8
+ CUSTOM_CODE: '62b5a2bc6343c2335f85946b',
9
+ WEBSITE_SECTION: '647d02f56348660726cc552d',
10
+ BASIC_DIV: '64d86d3563ed2a77be372c9a',
11
+ };
12
+ // ============================================================================
13
+ // HELPER FUNCTIONS
14
+ // ============================================================================
15
+ function generateUUID() {
16
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
17
+ let result = '';
18
+ for (let i = 0; i < 10; i++) {
19
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
20
+ }
21
+ return result;
22
+ }
23
+ async function fetchInterface(config, id) {
24
+ const response = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/${id}`, {
25
+ headers: {
26
+ 'Authorization': `Bearer ${config.accessToken}`,
27
+ 'Content-Type': 'application/json',
28
+ }
29
+ });
30
+ if (!response.ok) {
31
+ if (response.status === 403)
32
+ throw new Error('Permission denied. Ensure token has interface view permission.');
33
+ if (response.status === 404)
34
+ throw new Error(`Interface ${id} not found.`);
35
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
36
+ }
37
+ return response.json();
38
+ }
39
+ async function saveInterface(config, id, data) {
40
+ const response = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/${id}`, {
41
+ method: 'PUT',
42
+ headers: {
43
+ 'Authorization': `Bearer ${config.accessToken}`,
44
+ 'Content-Type': 'application/json',
45
+ },
46
+ body: JSON.stringify(data)
47
+ });
48
+ if (!response.ok) {
49
+ if (response.status === 403)
50
+ throw new Error('Permission denied. Ensure token has interface edit permission.');
51
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
52
+ }
53
+ return response.json();
54
+ }
55
+ function findRouteByName(routes, name) {
56
+ for (const route of routes) {
57
+ if (route.name === name)
58
+ return route;
59
+ if (route.routes && route.routes.length > 0) {
60
+ const found = findRouteByName(route.routes, name);
61
+ if (found)
62
+ return found;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function getAllRouteNames(routes) {
68
+ const names = [];
69
+ for (const route of routes) {
70
+ if (route.name)
71
+ names.push(route.name);
72
+ if (route.routes && route.routes.length > 0) {
73
+ names.push(...getAllRouteNames(route.routes));
74
+ }
75
+ }
76
+ return names;
77
+ }
78
+ function findSectionByUUID(data, uuid) {
79
+ // Search header sections
80
+ if (data.header?.sections) {
81
+ const found = searchSectionsForUUID(data.header.sections, uuid);
82
+ if (found)
83
+ return found;
84
+ }
85
+ // Search footer sections
86
+ if (data.footer?.sections) {
87
+ const found = searchSectionsForUUID(data.footer.sections, uuid);
88
+ if (found)
89
+ return found;
90
+ }
91
+ // Search all routes recursively
92
+ if (data.routes) {
93
+ const found = searchRouteSectionsForUUID(data.routes, uuid);
94
+ if (found)
95
+ return found;
96
+ }
97
+ // Search interface-level slots
98
+ if (data.slots) {
99
+ for (const slot of data.slots) {
100
+ if (slot.sections) {
101
+ const found = searchSectionsForUUID(slot.sections, uuid);
102
+ if (found)
103
+ return found;
104
+ }
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ function searchSectionsForUUID(sections, uuid) {
110
+ for (const section of sections) {
111
+ if (section.uuid === uuid)
112
+ return section;
113
+ if (section.slots) {
114
+ for (const slot of section.slots) {
115
+ if (slot.sections) {
116
+ const found = searchSectionsForUUID(slot.sections, uuid);
117
+ if (found)
118
+ return found;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ function searchRouteSectionsForUUID(routes, uuid) {
126
+ for (const route of routes) {
127
+ if (route.sections) {
128
+ const found = searchSectionsForUUID(route.sections, uuid);
129
+ if (found)
130
+ return found;
131
+ }
132
+ if (route.routes) {
133
+ const found = searchRouteSectionsForUUID(route.routes, uuid);
134
+ if (found)
135
+ return found;
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+ function removeSectionByUUID(data, uuid) {
141
+ // Search header
142
+ if (data.header?.sections) {
143
+ if (removeSectionFromArray(data.header.sections, uuid))
144
+ return true;
145
+ }
146
+ // Search footer
147
+ if (data.footer?.sections) {
148
+ if (removeSectionFromArray(data.footer.sections, uuid))
149
+ return true;
150
+ }
151
+ // Search routes
152
+ if (data.routes) {
153
+ if (removeFromRoutes(data.routes, uuid))
154
+ return true;
155
+ }
156
+ // Search interface-level slots
157
+ if (data.slots) {
158
+ for (const slot of data.slots) {
159
+ if (slot.sections && removeSectionFromArray(slot.sections, uuid))
160
+ return true;
161
+ }
162
+ }
163
+ return false;
164
+ }
165
+ function removeSectionFromArray(sections, uuid) {
166
+ const index = sections.findIndex((s) => s.uuid === uuid);
167
+ if (index !== -1) {
168
+ sections.splice(index, 1);
169
+ return true;
170
+ }
171
+ // Search nested slots
172
+ for (const section of sections) {
173
+ if (section.slots) {
174
+ for (const slot of section.slots) {
175
+ if (slot.sections && removeSectionFromArray(slot.sections, uuid))
176
+ return true;
177
+ }
178
+ }
179
+ }
180
+ return false;
181
+ }
182
+ function removeFromRoutes(routes, uuid) {
183
+ for (const route of routes) {
184
+ if (route.sections && removeSectionFromArray(route.sections, uuid))
185
+ return true;
186
+ if (route.routes && removeFromRoutes(route.routes, uuid))
187
+ return true;
188
+ }
189
+ return false;
190
+ }
191
+ function removeRouteByName(routes, name) {
192
+ const index = routes.findIndex((r) => r.name === name);
193
+ if (index !== -1) {
194
+ routes.splice(index, 1);
195
+ return true;
196
+ }
197
+ for (const route of routes) {
198
+ if (route.routes && removeRouteByName(route.routes, name))
199
+ return true;
200
+ }
201
+ return false;
202
+ }
203
+ function resolveTargetArray(data, target) {
204
+ switch (target.area) {
205
+ case 'header':
206
+ if (!data.header)
207
+ data.header = { sections: [] };
208
+ if (!data.header.sections)
209
+ data.header.sections = [];
210
+ return data.header.sections;
211
+ case 'footer':
212
+ if (!data.footer)
213
+ data.footer = { sections: [] };
214
+ if (!data.footer.sections)
215
+ data.footer.sections = [];
216
+ return data.footer.sections;
217
+ case 'route':
218
+ if (!target.routeName)
219
+ return null;
220
+ const route = findRouteByName(data.routes || [], target.routeName);
221
+ if (!route)
222
+ return null;
223
+ if (!route.sections)
224
+ route.sections = [];
225
+ return route.sections;
226
+ case 'slot':
227
+ if (!target.sectionUuid || !target.slotKey)
228
+ return null;
229
+ const section = findSectionByUUID(data, target.sectionUuid);
230
+ if (!section)
231
+ return null;
232
+ if (!section.slots)
233
+ section.slots = [];
234
+ let slot = section.slots.find((s) => s.key === target.slotKey);
235
+ if (!slot) {
236
+ slot = { title: target.slotKey, key: target.slotKey, sections: [] };
237
+ section.slots.push(slot);
238
+ }
239
+ if (!slot.sections)
240
+ slot.sections = [];
241
+ return slot.sections;
242
+ default:
243
+ return null;
244
+ }
245
+ }
246
+ function normalizeComponentId(id) {
247
+ if (!id)
248
+ return '';
249
+ if (typeof id === 'object')
250
+ return id._id || id;
251
+ return id;
252
+ }
253
+ function summarizeSections(sections, indent = '') {
254
+ const lines = [];
255
+ for (const section of sections || []) {
256
+ const compId = normalizeComponentId(section.componentID);
257
+ const shortId = compId.substring(0, 8);
258
+ const disabled = section.disabled ? ' [DISABLED]' : '';
259
+ lines.push(`${indent}- **${section.title || 'Section'}** (uuid: \`${section.uuid}\`, component: \`${shortId}...\`)${disabled}`);
260
+ if (section.slots) {
261
+ for (const slot of section.slots) {
262
+ if (slot.sections && slot.sections.length > 0) {
263
+ lines.push(`${indent} Slot "${slot.key}" (${slot.sections.length} sections):`);
264
+ lines.push(...summarizeSections(slot.sections, indent + ' '));
265
+ }
266
+ }
267
+ }
268
+ }
269
+ return lines;
270
+ }
271
+ function summarizeRoutes(routes, indent = '') {
272
+ const lines = [];
273
+ for (const route of routes || []) {
274
+ if (route.type === 'folder') {
275
+ lines.push(`${indent}- **${route.title}** (folder)`);
276
+ if (route.routes) {
277
+ lines.push(...summarizeRoutes(route.routes, indent + ' '));
278
+ }
279
+ }
280
+ else {
281
+ const sectionCount = (route.sections || []).length;
282
+ lines.push(`${indent}- **${route.title}** (name: \`${route.name}\`, path: \`${route.path}\`, ${sectionCount} sections)`);
283
+ }
284
+ }
285
+ return lines;
286
+ }
287
+ function summarizeInterface(data) {
288
+ const lines = [];
289
+ lines.push(`# Interface: ${data.title || 'Untitled'}`);
290
+ lines.push(`**ID:** \`${data._id}\` | **Layout:** ${data.layout || 'default'}\n`);
291
+ // Routes
292
+ const routes = data.routes || [];
293
+ lines.push(`## Routes`);
294
+ if (routes.length > 0) {
295
+ lines.push(...summarizeRoutes(routes));
296
+ }
297
+ else {
298
+ lines.push('No routes defined.');
299
+ }
300
+ lines.push('');
301
+ // Header
302
+ lines.push(`## Header (${(data.header?.sections || []).length} sections)`);
303
+ if (data.header?.sections?.length > 0) {
304
+ lines.push(...summarizeSections(data.header.sections));
305
+ }
306
+ lines.push('');
307
+ // Footer
308
+ lines.push(`## Footer (${(data.footer?.sections || []).length} sections)`);
309
+ if (data.footer?.sections?.length > 0) {
310
+ lines.push(...summarizeSections(data.footer.sections));
311
+ }
312
+ lines.push('');
313
+ // Menus
314
+ const menus = data.menus || [];
315
+ lines.push(`## Menus (${menus.length})`);
316
+ for (const menu of menus) {
317
+ lines.push(`- **${menu.title}** (name: \`${menu.name}\`, ${(menu.items || []).length} items)`);
318
+ }
319
+ lines.push('');
320
+ // Custom Components
321
+ const components = data.components || [];
322
+ lines.push(`## Custom Components (${components.length})`);
323
+ for (const comp of components) {
324
+ lines.push(`- \`<${comp.name}/>\` - ${comp.title || comp.name}`);
325
+ }
326
+ lines.push('');
327
+ // Services
328
+ const services = data.services || [];
329
+ lines.push(`## Services (${services.length})`);
330
+ for (const svc of services) {
331
+ lines.push(`- **${svc.title || svc.name}** (name: \`${svc.name}\`)`);
332
+ }
333
+ lines.push('');
334
+ // Styles
335
+ const styles = data.styles || {};
336
+ const hasPre = styles.pre && styles.pre.trim().length > 0;
337
+ const hasPost = styles.post && styles.post.trim().length > 0;
338
+ const themeCount = (styles.themes || []).length;
339
+ lines.push(`## Styles`);
340
+ lines.push(`- Pre-SCSS: ${hasPre ? 'yes' : 'none'}`);
341
+ lines.push(`- Post-SCSS: ${hasPost ? 'yes' : 'none'}`);
342
+ lines.push(`- Themes: ${themeCount}`);
343
+ return lines.join('\n');
344
+ }
345
+ function formatSectionDetail(section) {
346
+ const lines = [];
347
+ const compId = normalizeComponentId(section.componentID);
348
+ lines.push(`## Section: ${section.title || 'Untitled'}`);
349
+ lines.push(`- **UUID:** \`${section.uuid}\``);
350
+ lines.push(`- **Component ID:** \`${compId}\``);
351
+ lines.push(`- **Version:** ${section.componentVersion || 'latest'}`);
352
+ lines.push(`- **Disabled:** ${section.disabled || false}`);
353
+ lines.push(`- **Themes:** ${JSON.stringify(section.themes || [])}`);
354
+ lines.push('');
355
+ lines.push(`### Model`);
356
+ lines.push('```json');
357
+ lines.push(JSON.stringify(section.model || {}, null, 2));
358
+ lines.push('```');
359
+ lines.push('');
360
+ if (section.slots && section.slots.length > 0) {
361
+ lines.push(`### Slots`);
362
+ for (const slot of section.slots) {
363
+ lines.push(`**${slot.title || slot.key}** (key: \`${slot.key}\`, ${(slot.sections || []).length} sections)`);
364
+ lines.push(...summarizeSections(slot.sections || [], ' '));
365
+ }
366
+ }
367
+ return lines.join('\n');
368
+ }
369
+ async function getConfigAndFetch(interfaceId) {
370
+ const configManager = new ConfigManager();
371
+ const config = await configManager.loadConfig();
372
+ if (!config)
373
+ throw new Error('Qik MCP server not configured. Run setup first.');
374
+ if (!interfaceId)
375
+ throw new Error('interfaceId is required');
376
+ const data = await fetchInterface(config, interfaceId);
377
+ return { config, data };
378
+ }
379
+ function createScopeSelectionPrompt(contentType, availableScopes, userSession) {
380
+ const scopeOptions = availableScopes.map(scopeId => {
381
+ const title = getScopeTitle(userSession, scopeId);
382
+ return `- **${title}** (ID: \`${scopeId}\`)`;
383
+ }).join('\n');
384
+ return {
385
+ content: [{
386
+ type: "text",
387
+ text: `Multiple scopes available for creating ${contentType}. Please specify one:\n\n${scopeOptions}\n\nCall again with the \`scope\` parameter set to the desired scope ID.`
388
+ }]
389
+ };
390
+ }
391
+ // ============================================================================
392
+ // TOOL DEFINITIONS
393
+ // ============================================================================
394
+ export const createInterfaceTool = {
395
+ name: "create_interface",
396
+ description: `Create a new interface (website/app).
397
+
398
+ An interface is a visual page builder document containing routes, sections, menus, components, and styles.
399
+
400
+ After creating, use \`add_interface_route\`, \`add_interface_section\`, etc. to build it out.
401
+
402
+ **Example:**
403
+ \`\`\`json
404
+ { "title": "School Fair Website" }
405
+ \`\`\``,
16
406
  inputSchema: {
17
407
  type: "object",
18
408
  properties: {
19
- prompt: {
409
+ title: {
20
410
  type: "string",
21
- description: "Natural language description of the interface you want to build"
411
+ description: "Interface title (e.g., 'School Fair Website')"
22
412
  },
23
413
  scope: {
24
414
  type: "string",
25
- description: "The scope ID to create the interface in. If not provided and multiple scopes are available, you'll be prompted to select one."
415
+ description: "Scope ID to create in. Omit to auto-select if only one scope available."
416
+ }
417
+ },
418
+ required: ["title"]
419
+ }
420
+ };
421
+ export const getInterfaceTool = {
422
+ name: "get_interface",
423
+ description: `Get an interface's structure.
424
+
425
+ By default returns a **summary** (routes, menus, components, services, styles overview) to save context.
426
+ Use \`routeName\` to see full section details for a specific route.
427
+ Use \`sectionUuid\` to see full details for a specific section including its model.
428
+
429
+ **Examples:**
430
+ \`\`\`json
431
+ { "interfaceId": "abc123" }
432
+ { "interfaceId": "abc123", "routeName": "home" }
433
+ { "interfaceId": "abc123", "sectionUuid": "PBqx5sBIGc" }
434
+ \`\`\``,
435
+ inputSchema: {
436
+ type: "object",
437
+ properties: {
438
+ interfaceId: {
439
+ type: "string",
440
+ description: "The interface content ID"
26
441
  },
27
- primaryColor: {
442
+ routeName: {
28
443
  type: "string",
29
- description: "Optional primary color for the interface (hex code, e.g., '#4a90e2'). Defaults to a nice blue."
444
+ description: "Get full section details for this route"
30
445
  },
31
- includeAuth: {
32
- type: "boolean",
33
- description: "Force include authentication pages even if not mentioned in prompt. Defaults to auto-detect."
446
+ sectionUuid: {
447
+ type: "string",
448
+ description: "Get full details for this specific section"
34
449
  }
35
450
  },
36
- required: ["prompt"],
37
- additionalProperties: false
38
- },
451
+ required: ["interfaceId"]
452
+ }
39
453
  };
40
- /**
41
- * Handler for building interfaces from prompts
42
- */
43
- export async function handleBuildInterfaceFromPrompt(args) {
44
- try {
45
- const configManager = new ConfigManager();
46
- const config = await configManager.loadConfig();
47
- if (!config) {
48
- throw new Error('Qik MCP server not configured. Run setup first.');
49
- }
50
- console.log('🎨 Building interface structure from prompt...');
51
- // Step 1: Parse the natural language prompt
52
- const parsed = parseInterfacePrompt(args.prompt);
53
- console.log('📋 Parsed requirements:', {
54
- appName: parsed.appName,
55
- appType: parsed.appType,
56
- requiresAuth: parsed.requiresAuth,
57
- pages: parsed.pages.map(p => p.name),
58
- features: parsed.features
59
- });
60
- // Step 2: Fetch available components and glossary
61
- const [components, glossaryResponse] = await Promise.all([
62
- getMarketplaceComponents(config),
63
- fetchGlossary(config)
64
- ]);
65
- console.log(`📦 Found ${components.length} available components`);
66
- console.log(`📚 Found ${glossaryResponse.length} content types in glossary`);
67
- // Step 3: Generate routes from parsed pages
68
- const routes = await generateRoutes(parsed.pages, components, glossaryResponse, config);
69
- console.log(`🛣️ Generated ${routes.length} routes`);
70
- // Step 4: Add authentication pages if needed
71
- if (parsed.requiresAuth || args.includeAuth) {
72
- addAuthPages(routes, components);
73
- console.log('🔐 Added authentication pages');
74
- }
75
- // Step 5: Add legal pages (Privacy Policy, Terms)
76
- addLegalPages(routes, components);
77
- console.log('📄 Added legal pages');
78
- // Step 6: Generate menus
79
- const menus = generateMenus(routes);
80
- console.log(`📋 Generated ${menus.length} menus`);
81
- // Step 7: Generate styles
82
- const styles = generateInterfaceStyles(args.primaryColor);
83
- const header = generateHeader(menus, components);
84
- const footer = generateFooter(menus, components);
85
- console.log('✨ Interface structure complete, delegating to create_content...');
86
- // Step 8: Delegate to create_content with the built interface structure
87
- // This will handle scope selection, permissions, and creation
88
- return await handleCreateContent({
89
- typeKey: "interface",
90
- title: parsed.appName,
91
- seo: {
92
- title: parsed.appName,
93
- description: `${parsed.appName} - Built with Qik`
94
- },
95
- menus,
96
- services: [],
97
- components: [],
98
- styles,
99
- header,
100
- footer,
101
- slots: [],
102
- layout: '<div><header-slot /><route-slot /><footer-slot /></div>',
103
- routes,
104
- fields: [],
105
- scope: args.scope // Pass through scope if provided
106
- });
454
+ export const publishInterfaceTool = {
455
+ name: "publish_interface",
456
+ description: `Publish an interface snapshot. Creates a frozen version that can be deployed.
457
+
458
+ **Example:**
459
+ \`\`\`json
460
+ { "interfaceId": "abc123" }
461
+ \`\`\``,
462
+ inputSchema: {
463
+ type: "object",
464
+ properties: {
465
+ interfaceId: {
466
+ type: "string",
467
+ description: "The interface content ID"
468
+ }
469
+ },
470
+ required: ["interfaceId"]
107
471
  }
108
- catch (error) {
109
- console.error('❌ Interface build failed:', error);
110
- return {
111
- content: [{
112
- type: "text",
113
- text: `❌ Failed to build interface: ${error.message}\n\n${error.stack || ''}`
114
- }]
115
- };
472
+ };
473
+ export const addInterfaceRouteTool = {
474
+ name: "add_interface_route",
475
+ description: `Add a route (page) to an interface.
476
+
477
+ Routes can be nested inside folder-type routes using \`parentRouteName\`.
478
+ Dynamic segments use \`:param\` syntax (e.g., \`/article/:slug\`).
479
+
480
+ **Examples:**
481
+ \`\`\`json
482
+ { "interfaceId": "abc123", "title": "Home", "path": "/", "name": "home" }
483
+ { "interfaceId": "abc123", "title": "Article Detail", "path": "/articles/:slug", "name": "articleDetail" }
484
+ { "interfaceId": "abc123", "title": "Legal", "path": "", "name": "legal", "type": "folder" }
485
+ { "interfaceId": "abc123", "title": "Privacy", "path": "/privacy", "name": "privacy", "parentRouteName": "legal" }
486
+ \`\`\``,
487
+ inputSchema: {
488
+ type: "object",
489
+ properties: {
490
+ interfaceId: { type: "string", description: "The interface content ID" },
491
+ title: { type: "string", description: "Route title (e.g., 'About Us')" },
492
+ path: { type: "string", description: "URL path (e.g., '/about'). Use :param for dynamic segments." },
493
+ name: { type: "string", description: "Unique route name in camelCase (e.g., 'aboutUs')" },
494
+ type: { type: "string", enum: ["route", "folder"], description: "Route type. Default: 'route'" },
495
+ parentRouteName: { type: "string", description: "Name of parent folder route for nesting" },
496
+ headerDisabled: { type: "boolean", description: "Disable the global header on this route" },
497
+ footerDisabled: { type: "boolean", description: "Disable the global footer on this route" }
498
+ },
499
+ required: ["interfaceId", "title", "path", "name"]
116
500
  }
117
- }
118
- /**
119
- * Fetch glossary from API
120
- */
121
- async function fetchGlossary(config) {
122
- const response = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/glossary`, {
123
- headers: {
124
- 'Authorization': `Bearer ${config.accessToken}`,
125
- 'Content-Type': 'application/json',
501
+ };
502
+ export const updateInterfaceRouteTool = {
503
+ name: "update_interface_route",
504
+ description: `Update a route's properties.
505
+
506
+ **Example:**
507
+ \`\`\`json
508
+ { "interfaceId": "abc123", "routeName": "home", "title": "Home Page", "headerDisabled": true }
509
+ \`\`\``,
510
+ inputSchema: {
511
+ type: "object",
512
+ properties: {
513
+ interfaceId: { type: "string", description: "The interface content ID" },
514
+ routeName: { type: "string", description: "Name of the route to update" },
515
+ title: { type: "string" },
516
+ path: { type: "string" },
517
+ headerDisabled: { type: "boolean" },
518
+ footerDisabled: { type: "boolean" },
519
+ seo: {
520
+ type: "object",
521
+ properties: {
522
+ title: { type: "string" },
523
+ description: { type: "string" }
524
+ }
525
+ }
126
526
  },
127
- });
128
- if (!response.ok) {
129
- throw new Error(`Failed to fetch glossary: HTTP ${response.status}`);
130
- }
131
- const data = await response.json();
132
- return Array.isArray(data) ? data : (data.items || data.types || []);
133
- }
134
- /**
135
- * Add authentication pages to routes
136
- */
137
- function addAuthPages(routes, components) {
138
- const hasLogin = routes.some(r => r.name === 'userLogin' || r.name === 'login');
139
- const hasSignup = routes.some(r => r.name === 'userSignup' || r.name === 'signup');
140
- // Find auth component
141
- const authComponent = components.find(c => c.title.toLowerCase().includes('combined') &&
142
- c.title.toLowerCase().includes('auth')) || components.find(c => c.title.toLowerCase().includes('login'));
143
- if (!authComponent) {
144
- console.warn('⚠️ Auth component not found in marketplace');
145
- return;
146
- }
147
- if (!hasLogin) {
148
- routes.push(createAuthPage('Login', '/login', 'userLogin', authComponent, 'login'));
149
- }
150
- if (!hasSignup) {
151
- routes.push(createAuthPage('Signup', '/signup', 'userSignup', authComponent, 'signup'));
152
- }
153
- }
154
- /**
155
- * Create an authentication page
156
- */
157
- function createAuthPage(title, path, name, component, defaultState) {
158
- return {
159
- title,
160
- path,
161
- name,
162
- type: 'route',
163
- sections: [{
164
- title: component.title,
165
- uuid: generateUUID(),
166
- componentID: component._id,
167
- componentVersion: 'latest',
168
- model: {
169
- hideTitle: false,
170
- redirectTo: 'home',
171
- redirectFromIntent: true,
172
- defaultState
173
- },
174
- slots: []
175
- }],
176
- seo: {
177
- title,
178
- description: `${title} page`,
179
- sitemapDisabled: false
527
+ required: ["interfaceId", "routeName"]
528
+ }
529
+ };
530
+ export const removeInterfaceRouteTool = {
531
+ name: "remove_interface_route",
532
+ description: `Remove a route from an interface. All sections within the route will be lost.
533
+
534
+ **Example:**
535
+ \`\`\`json
536
+ { "interfaceId": "abc123", "routeName": "aboutUs" }
537
+ \`\`\``,
538
+ inputSchema: {
539
+ type: "object",
540
+ properties: {
541
+ interfaceId: { type: "string", description: "The interface content ID" },
542
+ routeName: { type: "string", description: "Name of the route to remove" }
180
543
  },
181
- headerDisabled: false,
182
- footerDisabled: false,
183
- contextVisibility: {
184
- hideAuthenticated: true,
185
- hideUnauthenticated: false
186
- }
187
- };
188
- }
189
- /**
190
- * Generate comprehensive interface styles
191
- */
192
- function generateInterfaceStyles(primaryColor) {
193
- const primary = primaryColor || '#4a90e2';
194
- return {
195
- pre: `:root {
196
- --primary: ${primary};
197
- --text: #333;
198
- --headings: #111;
199
- --bg: #fff;
200
- --bg-secondary: #f8f9fa;
201
- --border: #e0e0e0;
202
- --wrap-width: 1200px;
203
- --radius: 0.5rem;
204
- --shadow: 0 2px 8px rgba(0,0,0,0.1);
205
- }
206
-
207
- @media(prefers-color-scheme: dark) {
208
- :root {
209
- --bg: #1a1a1a;
210
- --bg-secondary: #2d2d2d;
211
- --text: #e0e0e0;
212
- --headings: #fff;
213
- --border: #404040;
214
- --shadow: 0 2px 8px rgba(0,0,0,0.3);
544
+ required: ["interfaceId", "routeName"]
215
545
  }
216
- }`,
217
- post: `body, html {
218
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
219
- font-size: clamp(14px, 1.2vw, 18px);
220
- line-height: 1.6;
221
- color: var(--text);
222
- background: var(--bg);
223
- margin: 0;
224
- padding: 0;
225
- }
226
-
227
- * {
228
- box-sizing: border-box;
229
- }
546
+ };
547
+ export const addInterfaceSectionTool = {
548
+ name: "add_interface_section",
549
+ description: `Add a block component section to an interface.
230
550
 
231
- /* Vertical and Horizontal Wrapping */
232
- .v-wrap {
233
- padding: 3rem 0;
234
- }
551
+ This is the core building tool. Place a marketplace component as a section in the header, footer, a route, or nested inside another section's slot.
552
+
553
+ **Target areas:**
554
+ - \`"header"\` - Interface header (appears on all routes)
555
+ - \`"footer"\` - Interface footer (appears on all routes)
556
+ - \`"route"\` - A specific route's content (requires \`routeName\`)
557
+ - \`"slot"\` - Nested inside another section's slot (requires \`sectionUuid\` and \`slotKey\`)
235
558
 
236
- .h-wrap {
237
- max-width: var(--wrap-width);
238
- margin: 0 auto;
239
- padding: 0 1.5rem;
559
+ **Common components:**
560
+ - Custom Code (\`62b5a2bc6343c2335f85946b\`) - model: { html, javascript, scss }
561
+ - Website Section (\`647d02f56348660726cc552d\`) - model: { outerClass, innerClass } - has slot "cells"
562
+ - Basic Div (\`64d86d3563ed2a77be372c9a\`) - has slot "content"
563
+
564
+ **Examples:**
565
+ \`\`\`json
566
+ {
567
+ "interfaceId": "abc123",
568
+ "target": { "area": "route", "routeName": "home" },
569
+ "componentId": "62b5a2bc6343c2335f85946b",
570
+ "title": "Hero Section",
571
+ "model": {
572
+ "html": "<div><h1>Welcome</h1></div>",
573
+ "javascript": "{}",
574
+ "scss": "h1 { font-size: 3em; }"
575
+ }
240
576
  }
577
+ \`\`\`
241
578
 
242
- /* Typography */
243
- h1, h2, h3, h4, h5, h6 {
244
- color: var(--headings);
245
- line-height: 1.2;
246
- margin-top: 0;
247
- margin-bottom: 1rem;
248
- font-weight: 700;
579
+ \`\`\`json
580
+ {
581
+ "interfaceId": "abc123",
582
+ "target": { "area": "slot", "sectionUuid": "PBqx5sBIGc", "slotKey": "cells" },
583
+ "componentId": "62b5a2bc6343c2335f85946b",
584
+ "title": "Content Block"
249
585
  }
586
+ \`\`\`
250
587
 
251
- h1 { font-size: clamp(2rem, 5vw, 3rem); }
252
- h2 { font-size: clamp(1.5rem, 4vw, 2.5rem); }
253
- h3 { font-size: clamp(1.25rem, 3vw, 2rem); }
254
- h4 { font-size: 1.5rem; }
255
- h5 { font-size: 1.25rem; }
256
- h6 { font-size: 1.1rem; }
588
+ Returns the new section's UUID - save this for updating or nesting into its slots.`,
589
+ inputSchema: {
590
+ type: "object",
591
+ properties: {
592
+ interfaceId: { type: "string", description: "The interface content ID" },
593
+ target: {
594
+ type: "object",
595
+ description: "Where to place the section",
596
+ properties: {
597
+ area: {
598
+ type: "string",
599
+ enum: ["header", "footer", "route", "slot"],
600
+ description: "Target area"
601
+ },
602
+ routeName: { type: "string", description: "Required when area='route'" },
603
+ sectionUuid: { type: "string", description: "Required when area='slot' - parent section UUID" },
604
+ slotKey: { type: "string", description: "Required when area='slot' - slot key (e.g., 'cells', 'content')" }
605
+ },
606
+ required: ["area"]
607
+ },
608
+ componentId: {
609
+ type: "string",
610
+ description: "Component ObjectId. Common: Custom Code='62b5a2bc6343c2335f85946b', Website Section='647d02f56348660726cc552d', Basic Div='64d86d3563ed2a77be372c9a'"
611
+ },
612
+ title: { type: "string", description: "Display title for this section" },
613
+ model: {
614
+ type: "object",
615
+ description: "Field values for the component. For Custom Code: { html, javascript, scss }. Use get_interface_component_details to see fields."
616
+ },
617
+ componentVersion: { type: "string", description: "Component version: 'latest' (default) or 'draft'" }
618
+ },
619
+ required: ["interfaceId", "target", "componentId"]
620
+ }
621
+ };
622
+ export const updateInterfaceSectionTool = {
623
+ name: "update_interface_section",
624
+ description: `Update a section's model, title, or other properties.
257
625
 
258
- p {
259
- margin: 0 0 1rem 0;
260
- }
626
+ Model fields are **shallow-merged**: only the fields you provide are updated, others are preserved.
627
+ For example, to update just the HTML of a Custom Code section, send \`model: { html: "<new>" }\` and javascript/scss remain unchanged.
261
628
 
262
- a {
263
- color: var(--primary);
264
- text-decoration: none;
265
- transition: opacity 0.2s;
629
+ **Example:**
630
+ \`\`\`json
631
+ {
632
+ "interfaceId": "abc123",
633
+ "sectionUuid": "PBqx5sBIGc",
634
+ "model": { "html": "<div>Updated content</div>" }
266
635
  }
636
+ \`\`\``,
637
+ inputSchema: {
638
+ type: "object",
639
+ properties: {
640
+ interfaceId: { type: "string", description: "The interface content ID" },
641
+ sectionUuid: { type: "string", description: "UUID of the section to update" },
642
+ model: { type: "object", description: "Updated field values (merged with existing)" },
643
+ title: { type: "string", description: "New title" },
644
+ disabled: { type: "boolean", description: "Disable/enable the section" },
645
+ themes: { type: "array", items: { type: "string" }, description: "Theme UUIDs to apply" }
646
+ },
647
+ required: ["interfaceId", "sectionUuid"]
648
+ }
649
+ };
650
+ export const removeInterfaceSectionTool = {
651
+ name: "remove_interface_section",
652
+ description: `Remove a section by UUID. Searches header, footer, all routes, and nested slots.
267
653
 
268
- a:hover {
269
- opacity: 0.8;
270
- }
654
+ **Example:**
655
+ \`\`\`json
656
+ { "interfaceId": "abc123", "sectionUuid": "PBqx5sBIGc" }
657
+ \`\`\``,
658
+ inputSchema: {
659
+ type: "object",
660
+ properties: {
661
+ interfaceId: { type: "string", description: "The interface content ID" },
662
+ sectionUuid: { type: "string", description: "UUID of the section to remove" }
663
+ },
664
+ required: ["interfaceId", "sectionUuid"]
665
+ }
666
+ };
667
+ export const setInterfaceCustomComponentTool = {
668
+ name: "set_interface_custom_component",
669
+ description: `Add or update an inline custom component in the interface.
271
670
 
272
- /* Utility Classes */
273
- .font-xs { font-size: 0.75rem; }
274
- .font-sm { font-size: 0.875rem; }
275
- .font-lg { font-size: 1.25rem; }
276
- .font-xl { font-size: 1.5rem; }
277
- .font-muted { opacity: 0.6; }
671
+ Custom components are defined with HTML, JS, and CSS and can be used as tags in other component templates (e.g., \`<event-card />\`).
278
672
 
279
- .text-center { text-align: center; }
280
- .text-right { text-align: right; }
673
+ **JS format:** Vue Options API as a plain object \`{}\` without \`export default\`.
281
674
 
282
- /* Responsive Grid */
283
- .grid {
284
- display: grid;
285
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
286
- gap: 2rem;
675
+ **Example:**
676
+ \`\`\`json
677
+ {
678
+ "interfaceId": "abc123",
679
+ "name": "event-card",
680
+ "title": "Event Card",
681
+ "html": "<div class=\\"card\\"><h3>{{item.title}}</h3><p>{{item.data?.description}}</p></div>",
682
+ "js": "{ props: { item: { type: Object } } }",
683
+ "css": ".card { padding: 1em; border: 1px solid; border-radius: 0.3em; }"
287
684
  }
685
+ \`\`\`
288
686
 
289
- /* Card Component */
290
- .card {
291
- background: var(--bg-secondary);
292
- border-radius: var(--radius);
293
- padding: 1.5rem;
294
- box-shadow: var(--shadow);
295
- }
687
+ The component is then available as \`<event-card :item="myItem" />\` in Custom Code sections or other inline components.`,
688
+ inputSchema: {
689
+ type: "object",
690
+ properties: {
691
+ interfaceId: { type: "string", description: "The interface content ID" },
692
+ name: { type: "string", description: "Component tag name in kebab-case (e.g., 'event-card')" },
693
+ title: { type: "string", description: "Display title" },
694
+ html: { type: "string", description: "Vue template HTML" },
695
+ js: { type: "string", description: "Vue Options API object as plain {} (no export default)" },
696
+ css: { type: "string", description: "CSS/SCSS styles" }
697
+ },
698
+ required: ["interfaceId", "name", "html"]
699
+ }
700
+ };
701
+ export const setInterfaceServiceTool = {
702
+ name: "set_interface_service",
703
+ description: `Add or update a JavaScript service in the interface.
296
704
 
297
- /* Button Styles */
298
- button, .btn {
299
- background: var(--primary);
300
- color: white;
301
- border: none;
302
- padding: 0.75rem 1.5rem;
303
- border-radius: var(--radius);
304
- cursor: pointer;
305
- font-size: 1rem;
306
- transition: all 0.2s;
307
- }
705
+ Services are reactive Vue instances available as \`this.$sdk.app.services.serviceName\` in all components.
706
+ Use for shared state, API calls, or computed data.
707
+
708
+ **JS format:** Vue Options API as a plain object \`{}\` without \`export default\`.
308
709
 
309
- button:hover, .btn:hover {
310
- transform: translateY(-2px);
311
- box-shadow: var(--shadow);
710
+ **Example:**
711
+ \`\`\`json
712
+ {
713
+ "interfaceId": "abc123",
714
+ "name": "eventService",
715
+ "title": "Event Service",
716
+ "js": "{ data() { return { events: [], loading: false } }, created() { this.reload() }, methods: { async reload() { this.loading = true; const { items } = await $sdk.content.list('event'); this.events = items; this.loading = false; } } }"
312
717
  }
718
+ \`\`\``,
719
+ inputSchema: {
720
+ type: "object",
721
+ properties: {
722
+ interfaceId: { type: "string", description: "The interface content ID" },
723
+ name: { type: "string", description: "Service name in camelCase (e.g., 'eventService')" },
724
+ title: { type: "string", description: "Display title" },
725
+ js: { type: "string", description: "Vue Options API object as plain {}" }
726
+ },
727
+ required: ["interfaceId", "name", "js"]
728
+ }
729
+ };
730
+ export const setInterfaceMenuTool = {
731
+ name: "set_interface_menu",
732
+ description: `Create or update a navigation menu in the interface.
733
+
734
+ Menu items link to routes (by name) or external URLs. Items can be nested for dropdown menus.
313
735
 
314
- /* Form Styles */
315
- input, textarea, select {
316
- width: 100%;
317
- padding: 0.75rem;
318
- border: 1px solid var(--border);
319
- border-radius: var(--radius);
320
- background: var(--bg);
321
- color: var(--text);
322
- font-size: 1rem;
736
+ **Example:**
737
+ \`\`\`json
738
+ {
739
+ "interfaceId": "abc123",
740
+ "name": "primaryMenu",
741
+ "title": "Primary Menu",
742
+ "items": [
743
+ { "title": "Home", "type": "route", "route": "home" },
744
+ { "title": "About", "type": "route", "route": "about" },
745
+ { "title": "Docs", "type": "url", "url": "https://docs.example.com", "target": "_blank" }
746
+ ]
323
747
  }
748
+ \`\`\``,
749
+ inputSchema: {
750
+ type: "object",
751
+ properties: {
752
+ interfaceId: { type: "string", description: "The interface content ID" },
753
+ name: { type: "string", description: "Menu key/name (e.g., 'primaryMenu', 'footerMenu')" },
754
+ title: { type: "string", description: "Menu title" },
755
+ items: {
756
+ type: "array",
757
+ description: "Menu items",
758
+ items: {
759
+ type: "object",
760
+ properties: {
761
+ title: { type: "string", description: "Display text" },
762
+ type: { type: "string", enum: ["route", "url"], description: "Link type. Default: 'route'" },
763
+ route: { type: "string", description: "Route name (when type='route')" },
764
+ url: { type: "string", description: "URL (when type='url')" },
765
+ target: { type: "string", description: "Window target (e.g., '_blank')" },
766
+ items: { type: "array", description: "Submenu items (same structure)" }
767
+ },
768
+ required: ["title"]
769
+ }
770
+ }
771
+ },
772
+ required: ["interfaceId", "name", "title", "items"]
773
+ }
774
+ };
775
+ export const setInterfaceStylesTool = {
776
+ name: "set_interface_styles",
777
+ description: `Update the interface's global SCSS styles and themes.
778
+
779
+ Only the provided fields are updated; others remain unchanged.
324
780
 
325
- input:focus, textarea:focus, select:focus {
326
- outline: none;
327
- border-color: var(--primary);
781
+ - **pre**: SCSS injected before component styles. Use for CSS custom properties and variables.
782
+ - **post**: SCSS injected after component styles. Use for global rules, fonts, and overrides.
783
+ - **themes**: Named CSS blocks that can be applied to sections via their themes array.
784
+
785
+ **Example:**
786
+ \`\`\`json
787
+ {
788
+ "interfaceId": "abc123",
789
+ "pre": ":root { --primary: #4F46E5; --text: #333; --headings: #111; --wrap-width: 1200px; }",
790
+ "post": "body { font-family: 'Inter', sans-serif; color: var(--text); } .h-wrap { max-width: var(--wrap-width); margin: auto; padding: 0 20px; } .v-wrap { padding: 2em 0; }"
328
791
  }
792
+ \`\`\``,
793
+ inputSchema: {
794
+ type: "object",
795
+ properties: {
796
+ interfaceId: { type: "string", description: "The interface content ID" },
797
+ pre: { type: "string", description: "SCSS before component styles (variables, mixins)" },
798
+ post: { type: "string", description: "SCSS after component styles (global rules, fonts)" },
799
+ themes: {
800
+ type: "array",
801
+ description: "Theme definitions",
802
+ items: {
803
+ type: "object",
804
+ properties: {
805
+ title: { type: "string" },
806
+ uuid: { type: "string", description: "Unique theme ID (10-char alphanumeric)" },
807
+ body: { type: "string", description: "CSS rules for this theme" }
808
+ },
809
+ required: ["title", "uuid", "body"]
810
+ }
811
+ }
812
+ },
813
+ required: ["interfaceId"]
814
+ }
815
+ };
816
+ export const setInterfaceLayoutTool = {
817
+ name: "set_interface_layout",
818
+ description: `Set the interface's layout template.
819
+
820
+ The default layout is \`<header-slot/><route-slot/><footer-slot/>\` (empty string = default).
821
+ Set to a custom component tag to use a custom layout (e.g., \`<custom-layout />\`).
822
+ The custom component must be defined via \`set_interface_custom_component\` first.
329
823
 
330
- /* Responsive */
331
- @media (max-width: 768px) {
332
- .v-wrap {
333
- padding: 2rem 0;
824
+ **Example:**
825
+ \`\`\`json
826
+ { "interfaceId": "abc123", "layout": "<custom-layout />" }
827
+ \`\`\``,
828
+ inputSchema: {
829
+ type: "object",
830
+ properties: {
831
+ interfaceId: { type: "string", description: "The interface content ID" },
832
+ layout: { type: "string", description: "Layout template. Empty string for default." }
833
+ },
834
+ required: ["interfaceId", "layout"]
334
835
  }
335
-
336
- .h-wrap {
337
- padding: 0 1rem;
836
+ };
837
+ // ============================================================================
838
+ // HANDLERS
839
+ // ============================================================================
840
+ export async function handleCreateInterface(args) {
841
+ try {
842
+ const configManager = new ConfigManager();
843
+ const config = await configManager.loadConfig();
844
+ if (!config) {
845
+ throw new Error('Qik MCP server not configured. Run setup first.');
846
+ }
847
+ if (!args.title) {
848
+ return createErrorResponse('title is required');
849
+ }
850
+ // Get user session and check permissions
851
+ const userSession = await getUserSessionData();
852
+ const createPermission = 'interface.create';
853
+ const availableScopes = getAvailableScopes(userSession, createPermission);
854
+ if (availableScopes.length === 0) {
855
+ return createErrorResponse(`You don't have permission to create interfaces in any scope.`);
856
+ }
857
+ const selectedScopeId = args.scope;
858
+ if (!selectedScopeId && availableScopes.length > 1) {
859
+ return createScopeSelectionPrompt('interface', availableScopes, userSession);
860
+ }
861
+ const targetScopeId = selectedScopeId || availableScopes[0];
862
+ if (!availableScopes.includes(targetScopeId)) {
863
+ return createErrorResponse(`Invalid scope "${targetScopeId}".`);
864
+ }
865
+ const payload = {
866
+ title: args.title,
867
+ meta: {
868
+ scopes: [targetScopeId],
869
+ status: 'active'
870
+ },
871
+ routes: [
872
+ {
873
+ title: 'Home',
874
+ path: '/',
875
+ name: 'home',
876
+ type: 'route',
877
+ sections: [],
878
+ routes: []
879
+ }
880
+ ],
881
+ header: { sections: [] },
882
+ footer: { sections: [] },
883
+ menus: [],
884
+ components: [],
885
+ services: [],
886
+ styles: { pre: '', post: '', themes: [], includes: [] },
887
+ };
888
+ const response = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/interface/create`, {
889
+ method: 'POST',
890
+ headers: {
891
+ 'Authorization': `Bearer ${config.accessToken}`,
892
+ 'Content-Type': 'application/json',
893
+ },
894
+ body: JSON.stringify(payload)
895
+ });
896
+ if (!response.ok) {
897
+ const errorText = await response.text();
898
+ throw new Error(`HTTP ${response.status} - ${errorText}`);
899
+ }
900
+ const result = await response.json();
901
+ const id = result._id || result.id;
902
+ return {
903
+ content: [{
904
+ type: "text",
905
+ text: `# Interface Created\n\n**Title:** ${args.title}\n**ID:** \`${id}\`\n\nA default "Home" route (path: /) has been created. Use \`get_interface\` to see the structure, then start adding routes and sections.`
906
+ }]
907
+ };
338
908
  }
339
-
340
- .grid {
341
- grid-template-columns: 1fr;
342
- gap: 1rem;
909
+ catch (error) {
910
+ return createErrorResponse(`Failed to create interface: ${error.message}`);
343
911
  }
344
912
  }
345
-
346
- /* Touch device optimization */
347
- @media (pointer: coarse) {
348
- * {
349
- -webkit-tap-highlight-color: transparent;
913
+ export async function handleGetInterface(args) {
914
+ try {
915
+ const { data } = await getConfigAndFetch(args.interfaceId);
916
+ // If requesting a specific section
917
+ if (args.sectionUuid) {
918
+ const section = findSectionByUUID(data, args.sectionUuid);
919
+ if (!section) {
920
+ return createErrorResponse(`Section with UUID '${args.sectionUuid}' not found.`);
921
+ }
922
+ return {
923
+ content: [{
924
+ type: "text",
925
+ text: formatSectionDetail(section)
926
+ }]
927
+ };
928
+ }
929
+ // If requesting a specific route's details
930
+ if (args.routeName) {
931
+ const route = findRouteByName(data.routes || [], args.routeName);
932
+ if (!route) {
933
+ const available = getAllRouteNames(data.routes || []);
934
+ return createErrorResponse(`Route '${args.routeName}' not found. Available: ${available.join(', ')}`);
935
+ }
936
+ const lines = [];
937
+ lines.push(`# Route: ${route.title}`);
938
+ lines.push(`**Name:** \`${route.name}\` | **Path:** \`${route.path}\` | **Type:** ${route.type || 'route'}`);
939
+ lines.push(`**Header disabled:** ${route.headerDisabled || false} | **Footer disabled:** ${route.footerDisabled || false}`);
940
+ lines.push('');
941
+ lines.push(`## Sections (${(route.sections || []).length})`);
942
+ for (const section of route.sections || []) {
943
+ lines.push(formatSectionDetail(section));
944
+ lines.push('---');
945
+ }
946
+ return {
947
+ content: [{
948
+ type: "text",
949
+ text: lines.join('\n')
950
+ }]
951
+ };
952
+ }
953
+ // Default: summary view
954
+ return {
955
+ content: [{
956
+ type: "text",
957
+ text: summarizeInterface(data)
958
+ }]
959
+ };
350
960
  }
351
-
352
- button, .btn, a {
353
- min-height: 44px;
354
- min-width: 44px;
961
+ catch (error) {
962
+ return createErrorResponse(`Failed to get interface: ${error.message}`);
355
963
  }
356
- }`,
357
- includes: [],
358
- themes: []
359
- };
360
964
  }
361
- /**
362
- * Generate header structure
363
- */
364
- function generateHeader(menus, components) {
365
- // Find menu component
366
- const menuComponent = components.find(c => c.title.toLowerCase().includes('horizontal') &&
367
- c.title.toLowerCase().includes('menu'));
368
- if (!menuComponent) {
369
- return { sections: [] };
965
+ export async function handlePublishInterface(args) {
966
+ try {
967
+ const configManager = new ConfigManager();
968
+ const config = await configManager.loadConfig();
969
+ if (!config)
970
+ throw new Error('Qik MCP server not configured. Run setup first.');
971
+ if (!args.interfaceId)
972
+ return createErrorResponse('interfaceId is required');
973
+ const response = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/interface/${args.interfaceId}/publish`, {
974
+ method: 'POST',
975
+ headers: {
976
+ 'Authorization': `Bearer ${config.accessToken}`,
977
+ 'Content-Type': 'application/json',
978
+ },
979
+ });
980
+ if (!response.ok) {
981
+ const errorText = await response.text();
982
+ throw new Error(`HTTP ${response.status} - ${errorText}`);
983
+ }
984
+ const result = await response.json();
985
+ return {
986
+ content: [{
987
+ type: "text",
988
+ text: `# Interface Published\n\n**Interface ID:** \`${args.interfaceId}\`\n**Snapshot ID:** \`${result._id || 'created'}\`\n\nThe interface snapshot has been published.`
989
+ }]
990
+ };
991
+ }
992
+ catch (error) {
993
+ return createErrorResponse(`Failed to publish interface: ${error.message}`);
370
994
  }
371
- return {
372
- sections: [{
373
- title: 'Header Navigation',
374
- uuid: generateUUID(),
375
- componentID: menuComponent._id,
376
- componentVersion: 'latest',
377
- model: {
378
- menu: 'primaryMenu',
379
- style: 'horizontal'
380
- },
381
- slots: []
382
- }]
383
- };
384
995
  }
385
- /**
386
- * Generate footer structure
387
- */
388
- function generateFooter(menus, components) {
389
- // Check if footer menu exists
390
- const hasFooterMenu = menus.some(m => m.name === 'footerMenu');
391
- if (!hasFooterMenu) {
392
- return { sections: [] };
393
- }
394
- // Find menu component
395
- const menuComponent = components.find(c => c.title.toLowerCase().includes('horizontal') &&
396
- c.title.toLowerCase().includes('menu'));
397
- if (!menuComponent) {
398
- return { sections: [] };
996
+ export async function handleAddInterfaceRoute(args) {
997
+ try {
998
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
999
+ if (!args.title)
1000
+ return createErrorResponse('title is required');
1001
+ if (!args.name)
1002
+ return createErrorResponse('name is required');
1003
+ if (args.path === undefined || args.path === null)
1004
+ return createErrorResponse('path is required');
1005
+ if (!data.routes)
1006
+ data.routes = [];
1007
+ // Validate unique name
1008
+ const existingNames = getAllRouteNames(data.routes);
1009
+ if (existingNames.includes(args.name)) {
1010
+ return createErrorResponse(`Route name '${args.name}' already exists. Available names: ${existingNames.join(', ')}`);
1011
+ }
1012
+ const newRoute = {
1013
+ title: args.title,
1014
+ path: args.path,
1015
+ name: args.name,
1016
+ type: args.type || 'route',
1017
+ sections: [],
1018
+ routes: [],
1019
+ };
1020
+ if (args.headerDisabled !== undefined)
1021
+ newRoute.headerDisabled = args.headerDisabled;
1022
+ if (args.footerDisabled !== undefined)
1023
+ newRoute.footerDisabled = args.footerDisabled;
1024
+ // Add to parent or top level
1025
+ if (args.parentRouteName) {
1026
+ const parent = findRouteByName(data.routes, args.parentRouteName);
1027
+ if (!parent) {
1028
+ return createErrorResponse(`Parent route '${args.parentRouteName}' not found. Available: ${existingNames.join(', ')}`);
1029
+ }
1030
+ if (!parent.routes)
1031
+ parent.routes = [];
1032
+ parent.routes.push(newRoute);
1033
+ }
1034
+ else {
1035
+ data.routes.push(newRoute);
1036
+ }
1037
+ await saveInterface(config, args.interfaceId, data);
1038
+ return {
1039
+ content: [{
1040
+ type: "text",
1041
+ text: `# Route Added\n\n**Title:** ${args.title}\n**Name:** \`${args.name}\`\n**Path:** \`${args.path}\`\n**Type:** ${newRoute.type}${args.parentRouteName ? `\n**Parent:** ${args.parentRouteName}` : ''}`
1042
+ }]
1043
+ };
1044
+ }
1045
+ catch (error) {
1046
+ return createErrorResponse(`Failed to add route: ${error.message}`);
399
1047
  }
400
- return {
401
- sections: [{
402
- title: 'Footer Navigation',
403
- uuid: generateUUID(),
404
- componentID: menuComponent._id,
405
- componentVersion: 'latest',
406
- model: {
407
- menu: 'footerMenu',
408
- style: 'horizontal'
409
- },
410
- slots: []
411
- }]
412
- };
413
1048
  }
414
- /**
415
- * Generate UUID
416
- */
417
- function generateUUID() {
418
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
419
- let result = '';
420
- for (let i = 0; i < 10; i++) {
421
- result += chars.charAt(Math.floor(Math.random() * chars.length));
1049
+ export async function handleUpdateInterfaceRoute(args) {
1050
+ try {
1051
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1052
+ if (!args.routeName)
1053
+ return createErrorResponse('routeName is required');
1054
+ const route = findRouteByName(data.routes || [], args.routeName);
1055
+ if (!route) {
1056
+ const available = getAllRouteNames(data.routes || []);
1057
+ return createErrorResponse(`Route '${args.routeName}' not found. Available: ${available.join(', ')}`);
1058
+ }
1059
+ if (args.title !== undefined)
1060
+ route.title = args.title;
1061
+ if (args.path !== undefined)
1062
+ route.path = args.path;
1063
+ if (args.headerDisabled !== undefined)
1064
+ route.headerDisabled = args.headerDisabled;
1065
+ if (args.footerDisabled !== undefined)
1066
+ route.footerDisabled = args.footerDisabled;
1067
+ if (args.seo) {
1068
+ if (!route.seo)
1069
+ route.seo = {};
1070
+ Object.assign(route.seo, args.seo);
1071
+ }
1072
+ await saveInterface(config, args.interfaceId, data);
1073
+ return {
1074
+ content: [{
1075
+ type: "text",
1076
+ text: `# Route Updated\n\n**Name:** \`${args.routeName}\`\n**Title:** ${route.title}\n**Path:** \`${route.path}\``
1077
+ }]
1078
+ };
1079
+ }
1080
+ catch (error) {
1081
+ return createErrorResponse(`Failed to update route: ${error.message}`);
1082
+ }
1083
+ }
1084
+ export async function handleRemoveInterfaceRoute(args) {
1085
+ try {
1086
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1087
+ if (!args.routeName)
1088
+ return createErrorResponse('routeName is required');
1089
+ if (!removeRouteByName(data.routes || [], args.routeName)) {
1090
+ const available = getAllRouteNames(data.routes || []);
1091
+ return createErrorResponse(`Route '${args.routeName}' not found. Available: ${available.join(', ')}`);
1092
+ }
1093
+ await saveInterface(config, args.interfaceId, data);
1094
+ return {
1095
+ content: [{
1096
+ type: "text",
1097
+ text: `# Route Removed\n\nRoute \`${args.routeName}\` has been removed.`
1098
+ }]
1099
+ };
1100
+ }
1101
+ catch (error) {
1102
+ return createErrorResponse(`Failed to remove route: ${error.message}`);
1103
+ }
1104
+ }
1105
+ export async function handleAddInterfaceSection(args) {
1106
+ try {
1107
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1108
+ if (!args.target?.area)
1109
+ return createErrorResponse('target.area is required (header, footer, route, or slot)');
1110
+ if (!args.componentId)
1111
+ return createErrorResponse('componentId is required');
1112
+ if (args.target.area === 'route' && !args.target.routeName) {
1113
+ return createErrorResponse('target.routeName is required when area is "route"');
1114
+ }
1115
+ if (args.target.area === 'slot') {
1116
+ if (!args.target.sectionUuid)
1117
+ return createErrorResponse('target.sectionUuid is required when area is "slot"');
1118
+ if (!args.target.slotKey)
1119
+ return createErrorResponse('target.slotKey is required when area is "slot"');
1120
+ }
1121
+ const uuid = generateUUID();
1122
+ const model = args.model ? parseNestedJsonStrings(args.model) : {};
1123
+ const section = {
1124
+ title: args.title || 'Section',
1125
+ uuid,
1126
+ componentID: args.componentId,
1127
+ componentVersion: args.componentVersion || 'latest',
1128
+ model,
1129
+ slots: [],
1130
+ themes: [],
1131
+ collapsed: false,
1132
+ };
1133
+ const targetArray = resolveTargetArray(data, args.target);
1134
+ if (!targetArray) {
1135
+ if (args.target.area === 'route') {
1136
+ const available = getAllRouteNames(data.routes || []);
1137
+ return createErrorResponse(`Route '${args.target.routeName}' not found. Available: ${available.join(', ')}`);
1138
+ }
1139
+ if (args.target.area === 'slot') {
1140
+ return createErrorResponse(`Section '${args.target.sectionUuid}' not found. Use get_interface to see current UUIDs.`);
1141
+ }
1142
+ return createErrorResponse(`Invalid target: ${JSON.stringify(args.target)}`);
1143
+ }
1144
+ targetArray.push(section);
1145
+ await saveInterface(config, args.interfaceId, data);
1146
+ const locationStr = args.target.area === 'route' ? `route "${args.target.routeName}"` :
1147
+ args.target.area === 'slot' ? `slot "${args.target.slotKey}" of section "${args.target.sectionUuid}"` :
1148
+ args.target.area;
1149
+ return {
1150
+ content: [{
1151
+ type: "text",
1152
+ text: `# Section Added\n\n**Title:** ${section.title}\n**UUID:** \`${uuid}\`\n**Component:** \`${args.componentId}\`\n**Location:** ${locationStr}\n\nUse this UUID to update the section or add nested sections to its slots.`
1153
+ }]
1154
+ };
1155
+ }
1156
+ catch (error) {
1157
+ return createErrorResponse(`Failed to add section: ${error.message}`);
1158
+ }
1159
+ }
1160
+ export async function handleUpdateInterfaceSection(args) {
1161
+ try {
1162
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1163
+ if (!args.sectionUuid)
1164
+ return createErrorResponse('sectionUuid is required');
1165
+ const section = findSectionByUUID(data, args.sectionUuid);
1166
+ if (!section) {
1167
+ return createErrorResponse(`Section '${args.sectionUuid}' not found. Use get_interface to see current UUIDs.`);
1168
+ }
1169
+ if (args.title !== undefined)
1170
+ section.title = args.title;
1171
+ if (args.disabled !== undefined)
1172
+ section.disabled = args.disabled;
1173
+ if (args.themes !== undefined)
1174
+ section.themes = args.themes;
1175
+ if (args.model) {
1176
+ const parsedModel = parseNestedJsonStrings(args.model);
1177
+ section.model = Object.assign(section.model || {}, parsedModel);
1178
+ }
1179
+ await saveInterface(config, args.interfaceId, data);
1180
+ return {
1181
+ content: [{
1182
+ type: "text",
1183
+ text: `# Section Updated\n\n**UUID:** \`${args.sectionUuid}\`\n**Title:** ${section.title}`
1184
+ }]
1185
+ };
1186
+ }
1187
+ catch (error) {
1188
+ return createErrorResponse(`Failed to update section: ${error.message}`);
1189
+ }
1190
+ }
1191
+ export async function handleRemoveInterfaceSection(args) {
1192
+ try {
1193
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1194
+ if (!args.sectionUuid)
1195
+ return createErrorResponse('sectionUuid is required');
1196
+ if (!removeSectionByUUID(data, args.sectionUuid)) {
1197
+ return createErrorResponse(`Section '${args.sectionUuid}' not found. Use get_interface to see current UUIDs.`);
1198
+ }
1199
+ await saveInterface(config, args.interfaceId, data);
1200
+ return {
1201
+ content: [{
1202
+ type: "text",
1203
+ text: `# Section Removed\n\nSection \`${args.sectionUuid}\` has been removed.`
1204
+ }]
1205
+ };
1206
+ }
1207
+ catch (error) {
1208
+ return createErrorResponse(`Failed to remove section: ${error.message}`);
1209
+ }
1210
+ }
1211
+ export async function handleSetInterfaceCustomComponent(args) {
1212
+ try {
1213
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1214
+ if (!args.name)
1215
+ return createErrorResponse('name is required (kebab-case tag name)');
1216
+ if (!args.html)
1217
+ return createErrorResponse('html is required');
1218
+ if (!data.components)
1219
+ data.components = [];
1220
+ const existing = data.components.find((c) => c.name === args.name);
1221
+ const isUpdate = !!existing;
1222
+ if (existing) {
1223
+ existing.html = args.html;
1224
+ if (args.js !== undefined)
1225
+ existing.js = args.js;
1226
+ if (args.css !== undefined)
1227
+ existing.css = args.css;
1228
+ if (args.title !== undefined)
1229
+ existing.title = args.title;
1230
+ }
1231
+ else {
1232
+ data.components.push({
1233
+ title: args.title || args.name,
1234
+ name: args.name,
1235
+ html: args.html,
1236
+ js: args.js || '{}',
1237
+ css: args.css || '',
1238
+ });
1239
+ }
1240
+ await saveInterface(config, args.interfaceId, data);
1241
+ return {
1242
+ content: [{
1243
+ type: "text",
1244
+ text: `# Custom Component ${isUpdate ? 'Updated' : 'Created'}\n\n**Tag:** \`<${args.name} />\`\n**Title:** ${args.title || args.name}\n\nUse \`<${args.name} />\` in Custom Code sections or other inline components.`
1245
+ }]
1246
+ };
1247
+ }
1248
+ catch (error) {
1249
+ return createErrorResponse(`Failed to set custom component: ${error.message}`);
1250
+ }
1251
+ }
1252
+ export async function handleSetInterfaceService(args) {
1253
+ try {
1254
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1255
+ if (!args.name)
1256
+ return createErrorResponse('name is required');
1257
+ if (!args.js)
1258
+ return createErrorResponse('js is required');
1259
+ if (!data.services)
1260
+ data.services = [];
1261
+ const existing = data.services.find((s) => s.name === args.name);
1262
+ const isUpdate = !!existing;
1263
+ if (existing) {
1264
+ existing.js = args.js;
1265
+ if (args.title !== undefined)
1266
+ existing.title = args.title;
1267
+ }
1268
+ else {
1269
+ data.services.push({
1270
+ title: args.title || args.name,
1271
+ name: args.name,
1272
+ js: args.js,
1273
+ });
1274
+ }
1275
+ await saveInterface(config, args.interfaceId, data);
1276
+ return {
1277
+ content: [{
1278
+ type: "text",
1279
+ text: `# Service ${isUpdate ? 'Updated' : 'Created'}\n\n**Name:** \`${args.name}\`\n**Title:** ${args.title || args.name}\n\nAccess via \`this.$sdk.app.services.${args.name}\` in components.`
1280
+ }]
1281
+ };
1282
+ }
1283
+ catch (error) {
1284
+ return createErrorResponse(`Failed to set service: ${error.message}`);
1285
+ }
1286
+ }
1287
+ export async function handleSetInterfaceMenu(args) {
1288
+ try {
1289
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1290
+ if (!args.name)
1291
+ return createErrorResponse('name is required');
1292
+ if (!args.title)
1293
+ return createErrorResponse('title is required');
1294
+ if (!args.items)
1295
+ return createErrorResponse('items is required');
1296
+ if (!data.menus)
1297
+ data.menus = [];
1298
+ const items = parseNestedJsonStrings(args.items);
1299
+ // Normalize menu items
1300
+ const normalizeItems = (menuItems) => {
1301
+ return (menuItems || []).map((item) => ({
1302
+ title: item.title || '',
1303
+ type: item.type || 'route',
1304
+ route: item.route || '',
1305
+ url: item.url || '',
1306
+ target: item.target || '',
1307
+ items: item.items ? normalizeItems(item.items) : [],
1308
+ }));
1309
+ };
1310
+ const normalizedItems = normalizeItems(items);
1311
+ const existing = data.menus.find((m) => m.name === args.name);
1312
+ const isUpdate = !!existing;
1313
+ if (existing) {
1314
+ existing.title = args.title;
1315
+ existing.items = normalizedItems;
1316
+ }
1317
+ else {
1318
+ data.menus.push({
1319
+ title: args.title,
1320
+ name: args.name,
1321
+ items: normalizedItems,
1322
+ });
1323
+ }
1324
+ await saveInterface(config, args.interfaceId, data);
1325
+ return {
1326
+ content: [{
1327
+ type: "text",
1328
+ text: `# Menu ${isUpdate ? 'Updated' : 'Created'}\n\n**Name:** \`${args.name}\`\n**Title:** ${args.title}\n**Items:** ${normalizedItems.length}`
1329
+ }]
1330
+ };
1331
+ }
1332
+ catch (error) {
1333
+ return createErrorResponse(`Failed to set menu: ${error.message}`);
1334
+ }
1335
+ }
1336
+ export async function handleSetInterfaceStyles(args) {
1337
+ try {
1338
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1339
+ if (!data.styles)
1340
+ data.styles = { pre: '', post: '', themes: [], includes: [] };
1341
+ if (args.pre !== undefined)
1342
+ data.styles.pre = args.pre;
1343
+ if (args.post !== undefined)
1344
+ data.styles.post = args.post;
1345
+ if (args.themes !== undefined)
1346
+ data.styles.themes = args.themes;
1347
+ await saveInterface(config, args.interfaceId, data);
1348
+ const updated = [];
1349
+ if (args.pre !== undefined)
1350
+ updated.push('pre-SCSS');
1351
+ if (args.post !== undefined)
1352
+ updated.push('post-SCSS');
1353
+ if (args.themes !== undefined)
1354
+ updated.push(`themes (${args.themes.length})`);
1355
+ return {
1356
+ content: [{
1357
+ type: "text",
1358
+ text: `# Styles Updated\n\nUpdated: ${updated.join(', ')}`
1359
+ }]
1360
+ };
1361
+ }
1362
+ catch (error) {
1363
+ return createErrorResponse(`Failed to set styles: ${error.message}`);
1364
+ }
1365
+ }
1366
+ export async function handleSetInterfaceLayout(args) {
1367
+ try {
1368
+ const { config, data } = await getConfigAndFetch(args.interfaceId);
1369
+ if (args.layout === undefined)
1370
+ return createErrorResponse('layout is required');
1371
+ data.layout = args.layout;
1372
+ await saveInterface(config, args.interfaceId, data);
1373
+ return {
1374
+ content: [{
1375
+ type: "text",
1376
+ text: `# Layout Updated\n\n**Layout:** ${args.layout || '(default: header-slot/route-slot/footer-slot)'}`
1377
+ }]
1378
+ };
1379
+ }
1380
+ catch (error) {
1381
+ return createErrorResponse(`Failed to set layout: ${error.message}`);
422
1382
  }
423
- return result;
424
1383
  }
425
1384
  //# sourceMappingURL=interface-builder.js.map