@locofy/mcp 1.0.5 → 1.0.7

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/build/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import express from 'express';
2
2
  import { z } from 'zod';
3
3
  import { PullComponentsToolSchema, runPullComponentsTool } from './tools/pullComponents.js';
4
+ import { PullFilesToolSchema, runPullFilesTool } from './tools/pullFiles.js';
4
5
  // Only start the API server if DEBUG_MODE is set
5
6
  if (process.env.DEBUG_MODE === 'true') {
6
7
  const app = express();
@@ -24,6 +25,23 @@ if (process.env.DEBUG_MODE === 'true') {
24
25
  }
25
26
  }
26
27
  });
28
+ // API endpoint for pullFiles
29
+ app.post('/api/pullFiles', async (req, res) => {
30
+ try {
31
+ const validatedArgs = PullFilesToolSchema.parse(req.body);
32
+ const result = await runPullFilesTool(validatedArgs);
33
+ res.json(result);
34
+ }
35
+ catch (error) {
36
+ if (error instanceof z.ZodError) {
37
+ res.status(400).json({ error: 'Invalid request body', details: error.errors });
38
+ }
39
+ else {
40
+ console.error('Error processing request:', error);
41
+ res.status(500).json({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' });
42
+ }
43
+ }
44
+ });
27
45
  // Start the server
28
46
  app.listen(port, () => {
29
47
  console.log(`Debug API server running on http://localhost:${port}`);
package/build/index.js CHANGED
@@ -3,6 +3,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { pullComponentsToolName, pullComponentsToolDescription, PullComponentsToolSchema, runPullComponentsTool } from './tools/pullComponents.js';
6
+ import { pullFilesToolName, pullFilesToolDescription, PullFilesToolSchema, runPullFilesTool } from './tools/pullFiles.js';
6
7
  import './api.js';
7
8
  const server = new Server({
8
9
  name: 'locofy-mcp',
@@ -36,6 +37,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
36
37
  required: ['componentNames', 'workspacePath'],
37
38
  },
38
39
  },
40
+ {
41
+ name: pullFilesToolName,
42
+ description: pullFilesToolDescription,
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ fileNames: {
47
+ type: 'array',
48
+ items: {
49
+ type: 'string',
50
+ },
51
+ description: 'The list of specific file names to be retrieved, without resolving their component dependencies.'
52
+ },
53
+ workspacePath: {
54
+ type: 'string',
55
+ description: 'The full path to the workspace.'
56
+ },
57
+ },
58
+ }
59
+ }
39
60
  ],
40
61
  };
41
62
  });
@@ -46,6 +67,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
46
67
  const validated = PullComponentsToolSchema.parse(args);
47
68
  return await runPullComponentsTool(validated);
48
69
  }
70
+ case pullFilesToolName: {
71
+ const validated = PullFilesToolSchema.parse(args);
72
+ return await runPullFilesTool(validated);
73
+ }
49
74
  default:
50
75
  throw new Error(`Unknown tool: ${name}`);
51
76
  }
@@ -77,10 +77,11 @@ export async function runPullComponentsTool(args) {
77
77
  }
78
78
  async function fetchDirectoryStructure(componentNames, projectID, personalAccessToken) {
79
79
  const encodedNames = componentNames.map(name => encodeURIComponent(name)).join(',');
80
- let baseURL = 'https://codegen-api.locofy.ai/mcp/generators/';
81
- if (process.env.IS_DEV === 'true') {
82
- baseURL = 'https://codegen-api.locofy.dev/mcp/generators/';
83
- }
80
+ // let baseURL = 'https://codegen-api.locofy.ai/mcp/generators/';
81
+ // if (process.env.IS_DEV === 'true') {
82
+ // baseURL = 'https://codegen-api.locofy.dev/mcp/generators/';
83
+ // }
84
+ const baseURL = 'https://codegen-api.locofy.dev/mcp/generators/';
84
85
  const url = baseURL + projectID + '?name=' + encodedNames;
85
86
  const headers = {
86
87
  'Accept': '*/*',
@@ -489,10 +490,10 @@ function processDirectoryStructure(directoryStructure) {
489
490
  collectChangeStatus(directoryStructure);
490
491
  // Then clean the structure
491
492
  cleanNode(directoryStructure);
492
- // Get the first child (the actual component data we want)
493
- const result = directoryStructure.children;
494
493
  // Add the component lists to the component data
495
- result.componentsToUpdate = [...new Set(componentsToUpdate)];
496
- result.componentsUnchanged = [...new Set(componentsUnchanged)];
497
- return result;
494
+ return {
495
+ directoryStructure: directoryStructure.children,
496
+ componentsToUpdate: [...new Set(componentsToUpdate)],
497
+ componentsUnchanged: [...new Set(componentsUnchanged)]
498
+ };
498
499
  }
@@ -0,0 +1,497 @@
1
+ import { z } from 'zod';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import axios from 'axios';
5
+ /**
6
+ * PullFiles tool
7
+ * - Returns a hardcoded representation of a directory structure
8
+ * - Contains nested folders, files, and file contents
9
+ */
10
+ export const pullFilesToolName = 'getLatestFileCode';
11
+ export const pullFilesToolDescription = `Cursor IDE uses this tool to retrieve individual file contents without component dependency management. Unlike the component tool that handles components and their dependencies as a unit, this tool focuses on specific files by name, retrieving only their content without dependency resolution. Use this tool when you need the raw content of specific files rather than complete components with their dependency trees. If codeChanged is true, the tool will prompt the user to accept the changes before updating the file. If codeChanged is false, the tool will not update the files.
12
+ `;
13
+ export const PullFilesToolSchema = z.object({
14
+ fileNames: z.array(z.string()).describe('The list of specific file names to be retrieved, without resolving their component dependencies.').max(10).min(1),
15
+ workspacePath: z.string().describe('The full path to the workspace.'),
16
+ });
17
+ export async function runPullFilesTool(args) {
18
+ let fileNames = args.fileNames;
19
+ // clean the file names, remove extensions
20
+ fileNames = fileNames.map(name => name.replace(/\.[^/.]+$/, ''));
21
+ const workspacePath = decodeURIComponent(args.workspacePath);
22
+ const projectID = process.env.PROJECT_ID;
23
+ const personalAccessToken = process.env.PERSONAL_ACCESS_TOKEN;
24
+ if (!projectID || !personalAccessToken) {
25
+ throw new Error('PROJECT_ID or PERSONAL_ACCESS_TOKEN is not set');
26
+ }
27
+ try {
28
+ const result = await fetchDirectoryStructure(fileNames, projectID, personalAccessToken);
29
+ if (result.success) {
30
+ // Prepend workspacePath to all filePath attributes
31
+ prepareFilePaths(result.data, workspacePath);
32
+ // Download and save any files referenced in the response
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 = processDirectoryStructure(result.data);
38
+ // write resultJSON to a file
39
+ if (process.env.DEBUG_MODE === 'true') {
40
+ fs.writeFileSync('result.json', JSON.stringify(cleanedResult, null, 2));
41
+ }
42
+ return {
43
+ content: [
44
+ {
45
+ type: 'text',
46
+ text: JSON.stringify(cleanedResult, null, 2)
47
+ }
48
+ ]
49
+ };
50
+ }
51
+ else {
52
+ // Return the error message from fetchDirectoryStructure
53
+ return {
54
+ content: [
55
+ {
56
+ type: 'text',
57
+ statusCode: result.statusCode || 'unknown',
58
+ text: JSON.stringify({
59
+ error: result.error,
60
+ }, null, 2)
61
+ }
62
+ ]
63
+ };
64
+ }
65
+ }
66
+ catch (error) {
67
+ console.error('Error fetching directory structure:', error);
68
+ return {
69
+ content: [
70
+ {
71
+ type: 'text',
72
+ text: JSON.stringify({ error: 'Failed to fetch directory structure ' + error }, null, 2)
73
+ }
74
+ ]
75
+ };
76
+ }
77
+ }
78
+ async function fetchDirectoryStructure(fileNames, projectID, personalAccessToken) {
79
+ const encodedNames = fileNames.map(name => encodeURIComponent(name)).join(',');
80
+ // let baseURL = 'https://codegen-api.locofy.ai/mcp/generators/';
81
+ // if (process.env.IS_DEV === 'true') {
82
+ // baseURL = 'https://codegen-api.locofy.dev/mcp/generators/';
83
+ // }
84
+ const baseURL = 'https://codegen-api.locofy.dev/mcp/generators/';
85
+ const url = baseURL + projectID + '?name=' + encodedNames;
86
+ const headers = {
87
+ 'Accept': '*/*',
88
+ 'Content-Type': 'application/json',
89
+ 'Authorization': 'Bearer ' + personalAccessToken
90
+ };
91
+ const data = {};
92
+ try {
93
+ const response = await axios.post(url, data, { headers, });
94
+ if (response.status !== 200) {
95
+ // Instead of throwing, return an error object
96
+ return {
97
+ success: false,
98
+ error: `API returned status code ${response.status}`,
99
+ statusCode: response.status
100
+ };
101
+ }
102
+ if (!response.data || !response.data.directoryTree) {
103
+ return {
104
+ success: false,
105
+ error: 'API response is missing directoryTree property',
106
+ statusCode: response.status
107
+ };
108
+ }
109
+ return {
110
+ success: true,
111
+ data: response.data.directoryTree
112
+ };
113
+ }
114
+ catch (error) {
115
+ if (axios.isAxiosError(error)) {
116
+ if (error.response) {
117
+ const statusCode = error.response.status;
118
+ let errorMessage = '';
119
+ if (statusCode === 401) {
120
+ errorMessage = 'Authentication failed: Invalid or expired token. Please check your mcp.json configuration in the .cursor folder to ensure your PERSONAL_ACCESS_TOKEN is correct. You can visit the Locofy project settings page to check or regenerate your token if needed.';
121
+ }
122
+ else if (statusCode === 500) {
123
+ errorMessage = 'Server error: The API service is experiencing issues';
124
+ }
125
+ else {
126
+ errorMessage = `API request failed with status ${statusCode}: ${error.message}`;
127
+ }
128
+ return {
129
+ success: false,
130
+ error: errorMessage,
131
+ statusCode: statusCode
132
+ };
133
+ }
134
+ else if (error.request) {
135
+ return {
136
+ success: false,
137
+ error: 'Network error: No response received from API',
138
+ statusCode: 'network_error'
139
+ };
140
+ }
141
+ }
142
+ console.error('API request failed:', error);
143
+ return {
144
+ success: false,
145
+ error: `Failed to fetch directory structure: ${error instanceof Error ? error.message : 'Unknown error'}`,
146
+ statusCode: 'unknown_error'
147
+ };
148
+ }
149
+ }
150
+ /**
151
+ * Traverses the directory structure and prepends workspacePath to all filePath attributes
152
+ * @param directoryStructure The directory structure from the API response
153
+ * @param workspacePath The workspace path to prepend to file paths
154
+ */
155
+ function prepareFilePaths(directoryStructure, workspacePath) {
156
+ const processNode = (node) => {
157
+ if (node.filePath) {
158
+ if (!path.isAbsolute(node.filePath)) {
159
+ node.filePath = path.join(workspacePath, node.filePath);
160
+ }
161
+ }
162
+ // Process all file paths in exportData
163
+ if (node.exportData) {
164
+ // Handle exportData.filePath
165
+ if (node.exportData.filePath) {
166
+ if (!path.isAbsolute(node.exportData.filePath)) {
167
+ node.exportData.filePath = path.join(workspacePath, node.exportData.filePath);
168
+ }
169
+ }
170
+ // Handle exportData.files array
171
+ if (node.exportData.files && Array.isArray(node.exportData.files)) {
172
+ for (const file of node.exportData.files) {
173
+ if (file.filePath && !path.isAbsolute(file.filePath)) {
174
+ file.filePath = path.join(workspacePath, file.filePath);
175
+ }
176
+ }
177
+ }
178
+ // Handle dependencies array
179
+ if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
180
+ for (const dependency of node.exportData.dependencies) {
181
+ // Process dependency filePath
182
+ if (dependency.filePath && !path.isAbsolute(dependency.filePath)) {
183
+ dependency.filePath = path.join(workspacePath, dependency.filePath);
184
+ }
185
+ // Process dependency files array
186
+ if (dependency.files && Array.isArray(dependency.files)) {
187
+ for (const file of dependency.files) {
188
+ if (file.filePath && !path.isAbsolute(file.filePath)) {
189
+ file.filePath = path.join(workspacePath, file.filePath);
190
+ }
191
+ }
192
+ }
193
+ // Recursively process any nested dependencies if they exist
194
+ if (dependency.dependencies && Array.isArray(dependency.dependencies)) {
195
+ for (const nestedDep of dependency.dependencies) {
196
+ processNode(nestedDep); // Recursively process nested dependencies
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ // Process content.filePath if it exists
203
+ if (node.content && node.content.filePath) {
204
+ if (!path.isAbsolute(node.content.filePath)) {
205
+ node.content.filePath = path.join(workspacePath, node.content.filePath);
206
+ }
207
+ }
208
+ // Process children recursively
209
+ if (node.children && Array.isArray(node.children)) {
210
+ for (const child of node.children) {
211
+ processNode(child);
212
+ }
213
+ }
214
+ };
215
+ // Start processing from the root
216
+ processNode(directoryStructure);
217
+ }
218
+ /**
219
+ * Downloads and saves asset files from the directory structure
220
+ * @param directoryStructure The directory structure from the API response
221
+ */
222
+ async function downloadAndSaveAssets(directoryStructure) {
223
+ // Process each file in the directory structure
224
+ const processNode = async (node) => {
225
+ /* eslint-enable */
226
+ if (node.type === 'file' && node.exportData && node.exportData.files) {
227
+ // For each file in the files array
228
+ for (const file of node.exportData.files) {
229
+ if (file.url && file.fileName) {
230
+ const filePath = file.filePath;
231
+ // Create the directory structure if it doesn't exist
232
+ const dirPath = path.dirname(filePath);
233
+ if (!fs.existsSync(dirPath)) {
234
+ fs.mkdirSync(dirPath, { recursive: true });
235
+ console.log(`Created directory: ${dirPath}`);
236
+ }
237
+ // Only download if file doesn't exist
238
+ if (!fs.existsSync(filePath)) {
239
+ try {
240
+ console.log(`Downloading ${file.fileName} from ${file.url}`);
241
+ const response = await axios.get(file.url, { responseType: 'arraybuffer' });
242
+ fs.writeFileSync(filePath, Buffer.from(response.data));
243
+ console.log(`Saved ${file.fileName} to ${filePath}`);
244
+ }
245
+ catch (error) {
246
+ console.error(`Error downloading ${file.fileName}:`, error);
247
+ }
248
+ }
249
+ else {
250
+ console.log(`File ${filePath} already exists, skipping download`);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ // Process children recursively
256
+ if (node.children && Array.isArray(node.children)) {
257
+ for (const child of node.children) {
258
+ await processNode(child);
259
+ }
260
+ }
261
+ };
262
+ // Start processing from the root
263
+ await processNode(directoryStructure);
264
+ }
265
+ /**
266
+ * Checks if the current code on disk differs from the code received from the API
267
+ * @param directoryStructure The directory structure from the API response
268
+ */
269
+ async function checkForCodeChanges(directoryStructure) {
270
+ // Process a single file and check if its content has changed
271
+ function checkFile(filePath, content) {
272
+ if (!fs.existsSync(filePath)) {
273
+ console.log(`New file: ${filePath}`);
274
+ return true;
275
+ }
276
+ try {
277
+ const fileContent = fs.readFileSync(filePath, 'utf8');
278
+ const codeChanged = hasCodeChanged(fileContent, content);
279
+ if (codeChanged) {
280
+ console.log(`File changed: ${filePath}`);
281
+ }
282
+ return codeChanged;
283
+ }
284
+ catch (error) {
285
+ console.error(`Error reading file ${filePath}:`, error);
286
+ return true; // Consider file as changed if we can't read it
287
+ }
288
+ }
289
+ // Check CSS module file if it exists
290
+ function checkCssFile(basePath, cssFileName, cssContent) {
291
+ if (!cssFileName || !cssContent)
292
+ return false;
293
+ const cssFilePath = path.join(path.dirname(basePath), cssFileName);
294
+ if (fs.existsSync(cssFilePath)) {
295
+ const cssFileContent = fs.readFileSync(cssFilePath, 'utf8');
296
+ const cssChanged = hasCodeChanged(cssFileContent, cssContent);
297
+ if (cssChanged) {
298
+ console.log(`CSS module changed: ${cssFilePath}`);
299
+ return true;
300
+ }
301
+ else {
302
+ return false;
303
+ }
304
+ }
305
+ return true; // CSS file doesn't exist, consider as new/changed
306
+ }
307
+ // Update node with change status and instructions
308
+ function updateNodeStatus(node, fileName, hasChanged, isCss = false) {
309
+ if (!hasChanged) {
310
+ const message = `This file ${fileName} has no changes, no need to update`;
311
+ if (isCss) {
312
+ node.cssContent = message;
313
+ node.instructionCSSForIDE = message;
314
+ }
315
+ else {
316
+ node.instructionForIDE = message;
317
+ node.content = message;
318
+ }
319
+ }
320
+ else if (!fs.existsSync(node.filePath)) {
321
+ node.instructionForIDE = 'This file does not exist, please create it';
322
+ }
323
+ return hasChanged;
324
+ }
325
+ // The main recursive function to process nodes
326
+ function processNode(node) {
327
+ let hasChanges = false;
328
+ // Remove experimental fields first
329
+ if (node.exportData) {
330
+ if (node.filePath) {
331
+ const nodeContent = node.name.includes('.css') ?
332
+ node.exportData.cssContent :
333
+ node.exportData.content;
334
+ const fileChanged = checkFile(node.filePath, nodeContent);
335
+ node.codeChanged = fileChanged;
336
+ hasChanges = hasChanges || fileChanged;
337
+ updateNodeStatus(node, node.name, fileChanged);
338
+ // Check associated CSS module
339
+ if (node.exportData.cssFileName && node.exportData.cssContent) {
340
+ const cssChanged = checkCssFile(node.filePath, node.exportData.cssFileName, node.exportData.cssContent);
341
+ hasChanges = hasChanges || cssChanged;
342
+ updateNodeStatus(node.exportData, node.exportData.cssFileName, cssChanged, true);
343
+ }
344
+ }
345
+ // Process dependencies
346
+ if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
347
+ for (const dependency of node.exportData.dependencies) {
348
+ // Process dependency file
349
+ if (dependency.filePath) {
350
+ const isCSS = dependency.name && dependency.name.includes('.css');
351
+ const depContent = isCSS ? dependency.cssContent : dependency.content;
352
+ const fileChanged = checkFile(dependency.filePath, depContent);
353
+ dependency.codeChanged = fileChanged;
354
+ hasChanges = hasChanges || fileChanged;
355
+ updateNodeStatus(dependency, dependency.compName, fileChanged);
356
+ // Check associated CSS module
357
+ if (dependency.cssFileName && dependency.cssContent) {
358
+ const cssChanged = checkCssFile(dependency.filePath, dependency.cssFileName, dependency.cssContent);
359
+ hasChanges = hasChanges || cssChanged;
360
+ updateNodeStatus(dependency, dependency.cssFileName, cssChanged, true);
361
+ }
362
+ }
363
+ // Process nested dependencies recursively
364
+ if (dependency.dependencies && Array.isArray(dependency.dependencies)) {
365
+ for (const nestedDep of dependency.dependencies) {
366
+ const nestedChanged = processNode(nestedDep);
367
+ hasChanges = hasChanges || nestedChanged;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ // Process children recursively
374
+ if (node.children && Array.isArray(node.children)) {
375
+ for (const child of node.children) {
376
+ const childChanged = processNode(child);
377
+ hasChanges = hasChanges || childChanged;
378
+ }
379
+ }
380
+ return hasChanges;
381
+ }
382
+ // Start processing from the root
383
+ return processNode(directoryStructure);
384
+ }
385
+ /**
386
+ * Compares two code snippets to determine if they are functionally different
387
+ * @param fileContent The code currently in the file
388
+ * @param apiCode The code provided by the API
389
+ * @returns True if the code has changed, false otherwise
390
+ */
391
+ function hasCodeChanged(fileContent, apiCode) {
392
+ if (!fileContent || !apiCode) {
393
+ return true;
394
+ }
395
+ // Normalize both code snippets
396
+ const normalizedFile = normalizeForComparison(fileContent);
397
+ const normalizedApi = normalizeForComparison(apiCode);
398
+ // Simple equality check on normalized versions
399
+ return normalizedFile !== normalizedApi;
400
+ }
401
+ /**
402
+ * Normalizes code for comparison by removing irrelevant differences
403
+ * @param code The code to normalize
404
+ * @returns Normalized code string
405
+ */
406
+ function normalizeForComparison(code) {
407
+ return code
408
+ .replace(/\s+/g, '') // Remove all whitespace (spaces, tabs, newlines)
409
+ .replace(/[;,'"]/g, '') // Remove semicolons, commas, and quotes
410
+ .replace(/\/\/.*$/gm, '') // Remove single-line comments
411
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
412
+ .toLowerCase(); // Convert to lowercase for case-insensitive comparison
413
+ }
414
+ /**
415
+ * Cleans the directory structure by removing unwanted properties from all nodes
416
+ * @param directoryStructure The directory structure to clean
417
+ * @returns The cleaned directory structure
418
+ */
419
+ function processDirectoryStructure(directoryStructure) {
420
+ const propertiesToRemove = [
421
+ 'files',
422
+ 'component',
423
+ 'jsLC',
424
+ 'assetCount',
425
+ 'cssLC',
426
+ 'componentCount',
427
+ 'pageMetaData',
428
+ 'isHomePage',
429
+ 'storybookFileName',
430
+ 'isAutoSyncNode'
431
+ ];
432
+ const filesToUpdate = [];
433
+ const filesUnchanged = [];
434
+ function collectChangeStatus(node) {
435
+ // Check if this is a component node with a name and change status
436
+ if (node.name && node.codeChanged !== undefined) {
437
+ if (node.codeChanged) {
438
+ filesToUpdate.push(node.name);
439
+ }
440
+ else {
441
+ filesUnchanged.push(node.name);
442
+ }
443
+ }
444
+ // Check dependencies
445
+ if (node.exportData && node.exportData.dependencies) {
446
+ node.exportData.dependencies.forEach((dep) => {
447
+ if (dep.compName && dep.codeChanged !== undefined) {
448
+ if (dep.codeChanged) {
449
+ filesToUpdate.push(dep.compName);
450
+ }
451
+ else {
452
+ filesUnchanged.push(dep.compName);
453
+ }
454
+ }
455
+ // Check nested dependencies
456
+ if (dep.dependencies && Array.isArray(dep.dependencies)) {
457
+ dep.dependencies.forEach((nestedDep) => collectChangeStatus(nestedDep));
458
+ }
459
+ });
460
+ }
461
+ // Process children recursively
462
+ if (node.children && Array.isArray(node.children)) {
463
+ node.children.forEach((child) => collectChangeStatus(child));
464
+ }
465
+ }
466
+ function cleanNode(node) {
467
+ // Remove unwanted properties from the current node
468
+ propertiesToRemove.forEach(prop => {
469
+ if (node[prop] !== undefined)
470
+ delete node[prop];
471
+ });
472
+ // Clean exportData if it exists
473
+ if (node.exportData) {
474
+ propertiesToRemove.forEach(prop => {
475
+ if (node.exportData[prop] !== undefined)
476
+ delete node.exportData[prop];
477
+ });
478
+ // Process dependencies in exportData
479
+ if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
480
+ node.exportData.dependencies.forEach((dep) => cleanNode(dep));
481
+ }
482
+ }
483
+ // Process children recursively
484
+ if (node.children && Array.isArray(node.children)) {
485
+ node.children.forEach((child) => cleanNode(child));
486
+ }
487
+ }
488
+ // First collect all change statuses
489
+ collectChangeStatus(directoryStructure);
490
+ // Then clean the structure
491
+ cleanNode(directoryStructure);
492
+ return {
493
+ directoryStructure: directoryStructure.children,
494
+ filesToUpdate: [...new Set(filesToUpdate)],
495
+ filesUnchanged: [...new Set(filesUnchanged)]
496
+ };
497
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locofy/mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Locofy MCP Server with Cursor",
5
5
  "keywords": [
6
6
  "figma",