@locofy/mcp 1.1.0 → 1.1.2
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 +1 -50
- package/build/constants.js +1 -7
- package/build/helpers/helpers.js +1 -31
- package/build/index.js +1 -88
- package/build/tools/pullComponents.js +1 -507
- package/build/tools/pullFiles.js +1 -505
- package/build/utils/analytics.js +1 -47
- package/package.json +9 -3
package/build/api.js
CHANGED
|
@@ -1,50 +1 @@
|
|
|
1
|
-
import express from
|
|
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{};
|
package/build/constants.js
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
const isDev
|
|
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";
|
package/build/helpers/helpers.js
CHANGED
|
@@ -1,31 +1 @@
|
|
|
1
|
-
import path from
|
|
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
|
|
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),source=(process.env.IDE_SOURCE||"other").toLowerCase();return track("mcp_execute_tool_e",{tool_name:pullComponentsToolName,project_id:projectID,user_id:userID,source:source}),{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)]}}
|
package/build/tools/pullFiles.js
CHANGED
|
@@ -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),source=(process.env.IDE_SOURCE||"other").toLowerCase();return track("mcp_execute_tool_e",{tool_name:pullFilesToolName,project_id:projectID,user_id:userID,source:source}),{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)]}}
|
package/build/utils/analytics.js
CHANGED
|
@@ -1,47 +1 @@
|
|
|
1
|
-
import
|
|
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.
|
|
3
|
+
"version": "1.1.2",
|
|
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
|
}
|