@locofy/mcp 1.1.4 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
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";import{CODEGEN_API_BASE_URL}from"../constants.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. Do not send a single string for this parameter. The values must be provided as a valid JSON array, with each string value enclosed in double quotes (e.g., ["Homepage", "DetailPage"]).').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(","),url=`${CODEGEN_API_BASE_URL}/mcp/generators/${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
+ 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";import{CODEGEN_API_BASE_URL}from"../constants.js";export const pullComponentsToolName="getLatestComponentAndDependencyCode";export const pullComponentsToolDescription="Coding Editers 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. Do not send a single string for this parameter. The values must be provided as a valid JSON array, with each string value enclosed in double quotes (e.g., ["Homepage", "DetailPage"]).').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(","),url=`${CODEGEN_API_BASE_URL}/mcp/generators/${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 +1 @@
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";import{CODEGEN_API_BASE_URL}from"../constants.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. Do not send a single string for this parameter. The values must be provided as a valid JSON array, with each string value enclosed in double quotes (e.g., ["Homepage.tsx", "DetailPage.tsx"]).').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(","),url=`${CODEGEN_API_BASE_URL}/mcp/generators/${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
+ 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";import{CODEGEN_API_BASE_URL}from"../constants.js";export const pullFilesToolName="getLatestFileCode";export const pullFilesToolDescription="Coding Editers 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. Do not send a single string for this parameter. The values must be provided as a valid JSON array, with each string value enclosed in double quotes (e.g., ["Homepage.tsx", "DetailPage.tsx"]).').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(","),url=`${CODEGEN_API_BASE_URL}/mcp/generators/${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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locofy/mcp",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Locofy MCP Server with Cursor",
5
5
  "keywords": [
6
6
  "figma",