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