@locofy/mcp 0.1.0 → 1.0.1

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.
@@ -7,14 +7,17 @@ import axios from 'axios';
7
7
  * - Returns a hardcoded representation of a directory structure
8
8
  * - Contains nested folders, files, and file contents
9
9
  */
10
- export const pullComponentsToolName = 'locofy';
11
- export const pullComponentsToolDescription = 'Provides a directory structure with nested folders, files, and their contents, allowing the MCP client to retrieve the code.';
10
+ export const pullComponentsToolName = 'getLatestComponentAndDependencyCode';
11
+ export const pullComponentsToolDescription = `Cursor IDE leverages this MCP tool to scan directory structures with nested folders, files, and their contents. It then compares this information with the current code in the IDE to intelligently update components and their dependencies. If codeChanged is true, the tool will prompt the user to accept the changes before updating the component and its dependencies. If codeChanged is false, the tool will not update the component and its dependencies. The tool ensures that users are informed of the components retrieved and their relationships, providing a clear understanding of the updates being made
12
+ `;
12
13
  export const PullComponentsToolSchema = z.object({
13
14
  componentNames: z.array(z.string()).describe('The list of component or screen names to be retrieved.').max(10).min(1),
14
15
  workspacePath: z.string().describe('The full path to the workspace.'),
15
16
  });
16
17
  export async function runPullComponentsTool(args) {
17
- const componentNames = args.componentNames;
18
+ let componentNames = args.componentNames;
19
+ // clean the component names, remove extensions
20
+ componentNames = componentNames.map(name => name.replace(/\.[^/.]+$/, ''));
18
21
  const workspacePath = decodeURIComponent(args.workspacePath);
19
22
  const projectID = process.env.PROJECT_ID;
20
23
  const personalAccessToken = process.env.PERSONAL_ACCESS_TOKEN;
@@ -24,13 +27,24 @@ export async function runPullComponentsTool(args) {
24
27
  try {
25
28
  const result = await fetchDirectoryStructure(componentNames, projectID, personalAccessToken);
26
29
  if (result.success) {
30
+ // Prepend workspacePath to all filePath attributes
31
+ prepareFilePaths(result.data, workspacePath);
27
32
  // Download and save any files referenced in the response
28
- await downloadAndSaveAssets(result.data, workspacePath);
33
+ await downloadAndSaveAssets(result.data);
34
+ // Check for code changes and mark changed files
35
+ await checkForCodeChanges(result.data);
36
+ // clean the directory structure
37
+ const cleanedResult = cleanDirectoryStructure(result.data);
38
+ const resultJSON = cleanedResult;
39
+ // write resultJSON to a file
40
+ if (process.env.DEBUG_MODE === 'true') {
41
+ fs.writeFileSync('result.json', JSON.stringify(resultJSON.children[0], null, 2));
42
+ }
29
43
  return {
30
44
  content: [
31
45
  {
32
46
  type: 'text',
33
- text: JSON.stringify(result.data, null, 2)
47
+ text: JSON.stringify(resultJSON.children[0], null, 2)
34
48
  }
35
49
  ]
36
50
  };
@@ -56,7 +70,7 @@ export async function runPullComponentsTool(args) {
56
70
  content: [
57
71
  {
58
72
  type: 'text',
59
- text: JSON.stringify({ error: 'Failed to fetch directory structure' }, null, 2)
73
+ text: JSON.stringify({ error: 'Failed to fetch directory structure ' + error }, null, 2)
60
74
  }
61
75
  ]
62
76
  };
@@ -64,7 +78,8 @@ export async function runPullComponentsTool(args) {
64
78
  }
65
79
  async function fetchDirectoryStructure(componentNames, projectID, personalAccessToken) {
66
80
  // TODO: change to PROD url
67
- const url = 'https://codegen-api.locofy.dev/mcp/generators/' + projectID + '?name=' + componentNames.join(',');
81
+ const encodedNames = componentNames.map(name => encodeURIComponent(name)).join(',');
82
+ const url = 'https://codegen-api.locofy.dev/mcp/generators/' + projectID + '?name=' + encodedNames;
68
83
  const headers = {
69
84
  'Accept': '*/*',
70
85
  'Content-Type': 'application/json',
@@ -129,13 +144,79 @@ async function fetchDirectoryStructure(componentNames, projectID, personalAccess
129
144
  };
130
145
  }
131
146
  }
147
+ /**
148
+ * Traverses the directory structure and prepends workspacePath to all filePath attributes
149
+ * @param directoryStructure The directory structure from the API response
150
+ * @param workspacePath The workspace path to prepend to file paths
151
+ */
152
+ function prepareFilePaths(directoryStructure, workspacePath) {
153
+ const processNode = (node) => {
154
+ if (node.filePath) {
155
+ if (!path.isAbsolute(node.filePath)) {
156
+ node.filePath = path.join(workspacePath, node.filePath);
157
+ }
158
+ }
159
+ // Process all file paths in exportData
160
+ if (node.exportData) {
161
+ // Handle exportData.filePath
162
+ if (node.exportData.filePath) {
163
+ if (!path.isAbsolute(node.exportData.filePath)) {
164
+ node.exportData.filePath = path.join(workspacePath, node.exportData.filePath);
165
+ }
166
+ }
167
+ // Handle exportData.files array
168
+ if (node.exportData.files && Array.isArray(node.exportData.files)) {
169
+ for (const file of node.exportData.files) {
170
+ if (file.filePath && !path.isAbsolute(file.filePath)) {
171
+ file.filePath = path.join(workspacePath, file.filePath);
172
+ }
173
+ }
174
+ }
175
+ // Handle dependencies array
176
+ if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
177
+ for (const dependency of node.exportData.dependencies) {
178
+ // Process dependency filePath
179
+ if (dependency.filePath && !path.isAbsolute(dependency.filePath)) {
180
+ dependency.filePath = path.join(workspacePath, dependency.filePath);
181
+ }
182
+ // Process dependency files array
183
+ if (dependency.files && Array.isArray(dependency.files)) {
184
+ for (const file of dependency.files) {
185
+ if (file.filePath && !path.isAbsolute(file.filePath)) {
186
+ file.filePath = path.join(workspacePath, file.filePath);
187
+ }
188
+ }
189
+ }
190
+ // Recursively process any nested dependencies if they exist
191
+ if (dependency.dependencies && Array.isArray(dependency.dependencies)) {
192
+ for (const nestedDep of dependency.dependencies) {
193
+ processNode(nestedDep); // Recursively process nested dependencies
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ // Process content.filePath if it exists
200
+ if (node.content && node.content.filePath) {
201
+ if (!path.isAbsolute(node.content.filePath)) {
202
+ node.content.filePath = path.join(workspacePath, node.content.filePath);
203
+ }
204
+ }
205
+ // Process children recursively
206
+ if (node.children && Array.isArray(node.children)) {
207
+ for (const child of node.children) {
208
+ processNode(child);
209
+ }
210
+ }
211
+ };
212
+ // Start processing from the root
213
+ processNode(directoryStructure);
214
+ }
132
215
  /**
133
216
  * Downloads and saves asset files from the directory structure
134
217
  * @param directoryStructure The directory structure from the API response
135
- * @param workspacePath The workspace path to save files to
136
218
  */
137
- /* eslint-disable */
138
- async function downloadAndSaveAssets(directoryStructure, workspacePath) {
219
+ async function downloadAndSaveAssets(directoryStructure) {
139
220
  // Process each file in the directory structure
140
221
  const processNode = async (node) => {
141
222
  /* eslint-enable */
@@ -143,7 +224,7 @@ async function downloadAndSaveAssets(directoryStructure, workspacePath) {
143
224
  // For each file in the files array
144
225
  for (const file of node.exportData.files) {
145
226
  if (file.url && file.fileName) {
146
- const filePath = path.join(workspacePath, file.filePath);
227
+ const filePath = file.filePath;
147
228
  // Create the directory structure if it doesn't exist
148
229
  const dirPath = path.dirname(filePath);
149
230
  if (!fs.existsSync(dirPath)) {
@@ -178,3 +259,197 @@ async function downloadAndSaveAssets(directoryStructure, workspacePath) {
178
259
  // Start processing from the root
179
260
  await processNode(directoryStructure);
180
261
  }
262
+ /**
263
+ * Checks if the current code on disk differs from the code received from the API
264
+ * @param directoryStructure The directory structure from the API response
265
+ */
266
+ async function checkForCodeChanges(directoryStructure) {
267
+ // Process a single file and check if its content has changed
268
+ function checkFile(filePath, content) {
269
+ if (!fs.existsSync(filePath)) {
270
+ console.log(`New file: ${filePath}`);
271
+ return true;
272
+ }
273
+ try {
274
+ const fileContent = fs.readFileSync(filePath, 'utf8');
275
+ const codeChanged = hasCodeChanged(fileContent, content);
276
+ if (codeChanged) {
277
+ console.log(`File changed: ${filePath}`);
278
+ }
279
+ return codeChanged;
280
+ }
281
+ catch (error) {
282
+ console.error(`Error reading file ${filePath}:`, error);
283
+ return true; // Consider file as changed if we can't read it
284
+ }
285
+ }
286
+ // Check CSS module file if it exists
287
+ function checkCssFile(basePath, cssFileName, cssContent) {
288
+ if (!cssFileName || !cssContent)
289
+ return false;
290
+ const cssFilePath = path.join(path.dirname(basePath), cssFileName);
291
+ if (fs.existsSync(cssFilePath)) {
292
+ const cssFileContent = fs.readFileSync(cssFilePath, 'utf8');
293
+ const cssChanged = hasCodeChanged(cssFileContent, cssContent);
294
+ if (cssChanged) {
295
+ console.log(`CSS module changed: ${cssFilePath}`);
296
+ return true;
297
+ }
298
+ else {
299
+ return false;
300
+ }
301
+ }
302
+ return true; // CSS file doesn't exist, consider as new/changed
303
+ }
304
+ // Update node with change status and instructions
305
+ function updateNodeStatus(node, fileName, hasChanged, isCss = false) {
306
+ if (!hasChanged) {
307
+ const message = `This file ${fileName} has no changes, no need to update`;
308
+ if (isCss) {
309
+ node.cssContent = message;
310
+ node.instructionCSSForIDE = message;
311
+ }
312
+ else {
313
+ node.instructionForIDE = message;
314
+ node.content = message;
315
+ }
316
+ }
317
+ else if (!fs.existsSync(node.filePath)) {
318
+ node.instructionForIDE = 'This file does not exist, please create it';
319
+ }
320
+ return hasChanged;
321
+ }
322
+ // The main recursive function to process nodes
323
+ function processNode(node) {
324
+ let hasChanges = false;
325
+ // Remove experimental fields first
326
+ if (node.exportData) {
327
+ // Process main component file
328
+ if (node.filePath) {
329
+ const nodeContent = node.name.includes('.css') ?
330
+ node.exportData.cssContent :
331
+ node.exportData.content;
332
+ const fileChanged = checkFile(node.filePath, nodeContent);
333
+ node.codeChanged = fileChanged;
334
+ hasChanges = hasChanges || fileChanged;
335
+ updateNodeStatus(node, node.name, fileChanged);
336
+ // Check associated CSS module
337
+ if (node.exportData.cssFileName && node.exportData.cssContent) {
338
+ const cssChanged = checkCssFile(node.filePath, node.exportData.cssFileName, node.exportData.cssContent);
339
+ hasChanges = hasChanges || cssChanged;
340
+ updateNodeStatus(node.exportData, node.exportData.cssFileName, cssChanged, true);
341
+ }
342
+ }
343
+ // Process dependencies
344
+ if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
345
+ for (const dependency of node.exportData.dependencies) {
346
+ // Process dependency file
347
+ if (dependency.filePath) {
348
+ const isCSS = dependency.name && dependency.name.includes('.css');
349
+ const depContent = isCSS ? dependency.cssContent : dependency.content;
350
+ const fileChanged = checkFile(dependency.filePath, depContent);
351
+ dependency.codeChanged = fileChanged;
352
+ hasChanges = hasChanges || fileChanged;
353
+ updateNodeStatus(dependency, dependency.compName, fileChanged);
354
+ // Check associated CSS module
355
+ if (dependency.cssFileName && dependency.cssContent) {
356
+ const cssChanged = checkCssFile(dependency.filePath, dependency.cssFileName, dependency.cssContent);
357
+ hasChanges = hasChanges || cssChanged;
358
+ updateNodeStatus(dependency, dependency.cssFileName, cssChanged, true);
359
+ }
360
+ }
361
+ // Process nested dependencies recursively
362
+ if (dependency.dependencies && Array.isArray(dependency.dependencies)) {
363
+ for (const nestedDep of dependency.dependencies) {
364
+ const nestedChanged = processNode(nestedDep);
365
+ hasChanges = hasChanges || nestedChanged;
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+ // Process children recursively
372
+ if (node.children && Array.isArray(node.children)) {
373
+ for (const child of node.children) {
374
+ const childChanged = processNode(child);
375
+ hasChanges = hasChanges || childChanged;
376
+ }
377
+ }
378
+ return hasChanges;
379
+ }
380
+ // Start processing from the root
381
+ return processNode(directoryStructure);
382
+ }
383
+ /**
384
+ * Compares two code snippets to determine if they are functionally different
385
+ * @param fileContent The code currently in the file
386
+ * @param apiCode The code provided by the API
387
+ * @returns True if the code has changed, false otherwise
388
+ */
389
+ function hasCodeChanged(fileContent, apiCode) {
390
+ if (!fileContent || !apiCode) {
391
+ return true;
392
+ }
393
+ // Normalize both code snippets
394
+ const normalizedFile = normalizeForComparison(fileContent);
395
+ const normalizedApi = normalizeForComparison(apiCode);
396
+ // Simple equality check on normalized versions
397
+ return normalizedFile !== normalizedApi;
398
+ }
399
+ /**
400
+ * Normalizes code for comparison by removing irrelevant differences
401
+ * @param code The code to normalize
402
+ * @returns Normalized code string
403
+ */
404
+ function normalizeForComparison(code) {
405
+ return code
406
+ .replace(/\s+/g, '') // Remove all whitespace (spaces, tabs, newlines)
407
+ .replace(/[;,'"]/g, '') // Remove semicolons, commas, and quotes
408
+ .replace(/\/\/.*$/gm, '') // Remove single-line comments
409
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
410
+ .toLowerCase(); // Convert to lowercase for case-insensitive comparison
411
+ }
412
+ /**
413
+ * Cleans the directory structure by removing unwanted properties from all nodes
414
+ * @param directoryStructure The directory structure to clean
415
+ * @returns The cleaned directory structure
416
+ */
417
+ function cleanDirectoryStructure(directoryStructure) {
418
+ const propertiesToRemove = [
419
+ 'files',
420
+ 'component',
421
+ 'jsLC',
422
+ 'assetCount',
423
+ 'cssLC',
424
+ 'componentCount',
425
+ 'pageMetaData',
426
+ 'isHomePage',
427
+ 'storybookFileName',
428
+ 'isAutoSyncNode'
429
+ ];
430
+ function cleanNode(node) {
431
+ // Remove unwanted properties from the current node
432
+ propertiesToRemove.forEach(prop => {
433
+ if (node[prop] !== undefined)
434
+ delete node[prop];
435
+ });
436
+ // Clean exportData if it exists
437
+ if (node.exportData) {
438
+ propertiesToRemove.forEach(prop => {
439
+ if (node.exportData[prop] !== undefined)
440
+ delete node.exportData[prop];
441
+ });
442
+ // Process dependencies in exportData
443
+ if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
444
+ node.exportData.dependencies.forEach((dep) => cleanNode(dep));
445
+ }
446
+ }
447
+ // Process children recursively
448
+ if (node.children && Array.isArray(node.children)) {
449
+ node.children.forEach((child) => cleanNode(child));
450
+ }
451
+ }
452
+ // Start cleaning from the root
453
+ cleanNode(directoryStructure);
454
+ return directoryStructure;
455
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locofy/mcp",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "description": "Locofy MCP Server with Cursor",
5
5
  "keywords": [
6
6
  "figma",