figma-context-mcp
Version:
Model Context Protocol server for Figma integration with smart position info
43 lines (39 loc) • 32.3 kB
JavaScript
import{McpServer as ve}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as D}from"zod";import O from"fs";import{isTruthy as Q}from"remeda";function I(e,t,o){if(!(typeof t=="object"&&t!==null)||!(e in t))return!1;let n=t[e];return o?o(n):n!==void 0}function v(e){return typeof e=="object"&&!!e&&"clipsContent"in e&&typeof e.clipsContent=="boolean"}function q(e){return typeof e=="object"&&!!e&&"absoluteBoundingBox"in e&&typeof e.absoluteBoundingBox=="object"&&!!e.absoluteBoundingBox&&"x"in e.absoluteBoundingBox&&"y"in e.absoluteBoundingBox&&"width"in e.absoluteBoundingBox&&"height"in e.absoluteBoundingBox}function V(e,t){let o=["HORIZONTAL","VERTICAL"];return v(t)&&o.includes(t.layoutMode??"NONE")&&q(e)&&e.layoutPositioning!=="ABSOLUTE"}function Y(e){return typeof e=="object"&&e!==null&&"top"in e&&"right"in e&&"bottom"in e&&"left"in e}function ee(e,t){let o=t;return typeof t=="object"&&!!t&&e in o&&typeof o[e]=="object"&&!!o[e]&&"x"in o[e]&&"y"in o[e]&&"width"in o[e]&&"height"in o[e]}function te(e){return Array.isArray(e)&&e.length===4&&e.every(t=>typeof t=="number")}import C from"fs";import ne from"path";async function ie(e,t,o,i){try{C.existsSync(t)||C.mkdirSync(t,{recursive:!0});let n=new Date().toISOString().replace(/[:.]/g,"-"),r=i?`figma-${o}-${i}-${n}.json`:`figma-${o}-${n}.json`,s=ne.join(t,r),l=JSON.stringify(e,null,2);return C.writeFileSync(s,l,"utf8"),s}catch(n){let r=n instanceof Error?n.message:String(n);throw new Error(`Error saving Figma data: ${r}`)}}async function Z(e,t,o,i,n,r){try{C.existsSync(t)||C.mkdirSync(t,{recursive:!0});let s=ne.join(t,e),l=await fetch(o,{method:"GET"});if(!l.ok)throw new Error(`Failed to download image: ${l.statusText}`);let d=C.createWriteStream(s),c=l.body?.getReader();if(!c)throw new Error("Failed to get response body");return new Promise((y,p)=>{let u=async()=>{try{for(;;){let{done:f,value:S}=await c.read();if(f){d.end();break}d.write(S)}}catch(f){d.end(),C.unlink(s,()=>{}),p(f)}};d.on("finish",()=>{i&&n&&r&&i.addDownloadUrl(n,e,r,o,"success"),y(s)}),d.on("error",f=>{c.cancel(),C.unlink(s,()=>{}),i&&n&&r&&i.addDownloadUrl(n,e,r,o,"failed"),p(new Error(`Failed to write image: ${f.message}`))}),u()})}catch(s){i&&n&&r&&i.addDownloadUrl(n,e,r,o,"failed");let l=s instanceof Error?s.message:String(s);throw new Error(`Error downloading image: ${l}`)}}function U(e){if(typeof e!="object"||e===null)return e;if(Array.isArray(e))return e.map(o=>U(o));let t={};for(let o in e)if(Object.prototype.hasOwnProperty.call(e,o)){let i=e[o],n=U(i);n!==void 0&&!(Array.isArray(n)&&n.length===0)&&!(typeof n=="object"&&n!==null&&Object.keys(n).length===0)&&(t[o]=n)}return t}function oe(e,t=1){let o=Math.round(e.r*255),i=Math.round(e.g*255),n=Math.round(e.b*255),r=Math.round(t*e.a*100)/100;return{hex:"#"+((1<<24)+(o<<16)+(i<<8)+n).toString(16).slice(1).toUpperCase(),opacity:r}}function z(e,t=1){let o=Math.round(e.r*255),i=Math.round(e.g*255),n=Math.round(e.b*255),r=Math.round(t*e.a*100)/100;return`rgba(${o}, ${i}, ${n}, ${r})`}function re(e="var"){let t="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",o="";for(let i=0;i<6;i++){let n=Math.floor(Math.random()*t.length);o+=t[n]}return`${e}_${o}`}function j(e,{ignoreZero:t=!0,suffix:o="px"}={}){let{top:i,right:n,bottom:r,left:s}=e;if(!(t&&i===0&&n===0&&r===0&&s===0))return i===n&&n===r&&r===s?`${i}${o}`:n===s?i===r?`${i}${o} ${n}${o}`:`${i}${o} ${n}${o} ${r}${o}`:`${i}${o} ${n}${o} ${r}${o} ${s}${o}`}function M(e){if(e.type==="IMAGE")return{type:"IMAGE",imageRef:e.imageRef,scaleMode:e.scaleMode};if(e.type==="SOLID"){let{hex:t,opacity:o}=oe(e.color,e.opacity);return o===1?t:z(e.color,o)}else{if(["GRADIENT_LINEAR","GRADIENT_RADIAL","GRADIENT_ANGULAR","GRADIENT_DIAMOND"].includes(e.type))return{type:e.type,gradientHandlePositions:e.gradientHandlePositions,gradientStops:e.gradientStops.map(({position:t,color:o})=>({position:t,color:oe(o)}))};throw new Error(`Unknown paint type: ${e.type}`)}}function T(e){return e.visible??!0}function L(e){if(isNaN(e))throw new TypeError("Input must be a valid number");return Number(Number(e).toFixed(2))}function le(e,t){let o=be(e),i=Ie(e,t,o.mode)||{};return{...o,...i}}function se(e,t){if(t&&t.mode!=="none"){let{children:o,mode:i,axis:n}=t,r=Se(n,i);if(o.length>0&&o.reduce((l,d)=>l?"layoutPositioning"in d&&d.layoutPositioning==="ABSOLUTE"?!0:r==="horizontal"?"layoutSizingHorizontal"in d&&d.layoutSizingHorizontal==="FILL":r==="vertical"?"layoutSizingVertical"in d&&d.layoutSizingVertical==="FILL":!1:!1,!0))return"stretch"}switch(e){case"MIN":return;case"MAX":return"flex-end";case"CENTER":return"center";case"SPACE_BETWEEN":return"space-between";case"BASELINE":return"baseline";default:return}}function ye(e){switch(e){case"MIN":return;case"MAX":return"flex-end";case"CENTER":return"center";case"STRETCH":return"stretch";default:return}}function ae(e){if(e==="FIXED")return"fixed";if(e==="FILL")return"fill";if(e==="HUG")return"hug"}function Se(e,t){switch(e){case"primary":switch(t){case"row":return"horizontal";case"column":return"vertical"}case"counter":switch(t){case"row":return"horizontal";case"column":return"vertical"}}}function be(e){if(!v(e))return{mode:"none"};let t={mode:!e.layoutMode||e.layoutMode==="NONE"?"none":e.layoutMode==="HORIZONTAL"?"row":"column"},o=[];return e.overflowDirection?.includes("HORIZONTAL")&&o.push("x"),e.overflowDirection?.includes("VERTICAL")&&o.push("y"),o.length>0&&(t.overflowScroll=o),e.clipsContent===!0&&(t.clipsContent=!0),t.mode==="none"||(t.justifyContent=se(e.primaryAxisAlignItems??"MIN",{children:e.children,axis:"primary",mode:t.mode}),t.alignItems=se(e.counterAxisAlignItems??"MIN",{children:e.children,axis:"counter",mode:t.mode}),t.alignSelf=ye(e.layoutAlign),t.wrap=e.layoutWrap==="WRAP"?!0:void 0,t.gap=e.itemSpacing?`${e.itemSpacing??0}px`:void 0,(e.paddingTop||e.paddingBottom||e.paddingLeft||e.paddingRight)&&(t.padding=j({top:e.paddingTop??0,right:e.paddingRight??0,bottom:e.paddingBottom??0,left:e.paddingLeft??0}))),t}function Ie(e,t,o){if(!q(e))return;let i={mode:o};if(i.sizing={horizontal:ae(e.layoutSizingHorizontal),vertical:ae(e.layoutSizingVertical)},v(t)&&!V(e,t)&&(e.layoutPositioning==="ABSOLUTE"&&(i.position="absolute"),e.absoluteBoundingBox&&t.absoluteBoundingBox&&(i.locationRelativeToParent={x:L(e.absoluteBoundingBox.x-t.absoluteBoundingBox.x),y:L(e.absoluteBoundingBox.y-t.absoluteBoundingBox.y)})),ee("absoluteBoundingBox",e)){let n={};o==="row"?(!e.layoutGrow&&e.layoutSizingHorizontal=="FIXED"&&(n.width=e.absoluteBoundingBox.width),e.layoutAlign!=="STRETCH"&&e.layoutSizingVertical=="FIXED"&&(n.height=e.absoluteBoundingBox.height)):o==="column"?(e.layoutAlign!=="STRETCH"&&e.layoutSizingHorizontal=="FIXED"&&(n.width=e.absoluteBoundingBox.width),!e.layoutGrow&&e.layoutSizingVertical=="FIXED"&&(n.height=e.absoluteBoundingBox.height),e.preserveRatio&&(n.aspectRatio=e.absoluteBoundingBox?.width/e.absoluteBoundingBox?.height)):((!e.layoutSizingHorizontal||e.layoutSizingHorizontal==="FIXED")&&(n.width=e.absoluteBoundingBox.width),(!e.layoutSizingVertical||e.layoutSizingVertical==="FIXED")&&(n.height=e.absoluteBoundingBox.height)),Object.keys(n).length>0&&(n.width&&(n.width=L(n.width)),n.height&&(n.height=L(n.height)),i.dimensions=n)}return i}function de(e){return Object.fromEntries(Object.entries(e).map(([t,o])=>[t,{id:t,key:o.key,name:o.name,componentSetId:o.componentSetId}]))}function ue(e){return Object.fromEntries(Object.entries(e).map(([t,o])=>[t,{id:t,key:o.key,name:o.name,description:o.description}]))}function ge(e){let t={colors:[]};return I("strokes",e)&&Array.isArray(e.strokes)&&e.strokes.length&&(t.colors=e.strokes.filter(T).map(M)),I("strokeWeight",e)&&typeof e.strokeWeight=="number"&&e.strokeWeight>0&&(t.strokeWeight=`${e.strokeWeight}px`),I("strokeDashes",e)&&Array.isArray(e.strokeDashes)&&e.strokeDashes.length&&(t.strokeDashes=e.strokeDashes),I("individualStrokeWeights",e,Y)&&(t.strokeWeights=j(e.individualStrokeWeights)),I("strokeAlign",e)&&typeof e.strokeAlign=="string"&&["INSIDE","OUTSIDE","CENTER"].includes(e.strokeAlign)&&(t.strokeAlign=e.strokeAlign),t}function fe(e){if(!I("effects",e))return{};let t=e.effects.filter(d=>d.visible),o=t.filter(d=>d.type==="DROP_SHADOW").map(we),i=t.filter(d=>d.type==="INNER_SHADOW").map($e),n=[...o,...i].join(", "),r=t.filter(d=>d.type==="LAYER_BLUR").map(ce).join(" "),s=t.filter(d=>d.type==="BACKGROUND_BLUR").map(ce).join(" "),l={};return n&&(e.type==="TEXT"?l.textShadow=n:l.boxShadow=n),r&&(l.filter=r),s&&(l.backdropFilter=s),l}function we(e){return`${e.offset.x}px ${e.offset.y}px ${e.radius}px ${e.spread??0}px ${z(e.color)}`}function $e(e){return`inset ${e.offset.x}px ${e.offset.y}px ${e.radius}px ${e.spread??0}px ${z(e.color)}`}function ce(e){return`blur(${e.radius}px)`}function xe(e,t){return v(e)&&e.layoutMode&&e.layoutMode!=="NONE"||t&&V(e,t)?!1:!!(t&&v(t)&&!(t.layoutMode&&t.layoutMode!=="NONE")||I("layoutPositioning",e)&&e.layoutPositioning==="ABSOLUTE"||!t)}function De(e,t){return t?{x:e.x-t.x,y:e.y-t.y}:{x:e.x,y:e.y}}function X(e){let t={},o={},i;if("nodes"in e){let f=Object.values(e.nodes);f.forEach(S=>{S.components&&Object.assign(t,S.components),S.componentSets&&Object.assign(o,S.componentSets)}),i=f.map(S=>S.document)}else Object.assign(t,e.components),Object.assign(o,e.componentSets),i=e.document.children;let n=de(t),r=ue(o),{name:s,lastModified:l,thumbnailUrl:d}=e,c={styles:{}},y;i.length>0&&I("absoluteBoundingBox",i[0])&&i[0].absoluteBoundingBox&&(y=i[0].absoluteBoundingBox);let p=i.filter(T).map(f=>me(c,f,void 0,y)).filter(f=>f!=null);return U({name:s,lastModified:l,thumbnailUrl:d||"",nodes:p,components:n,componentSets:r,globalVars:c})}function P(e,t,o){let[i]=Object.entries(e.styles).find(([r,s])=>JSON.stringify(s)===JSON.stringify(t))??[];if(i)return i;let n=re(o);return e.styles[n]=t,n}function me(e,t,o,i){let{id:n,name:r,type:s}=t,l={id:n,name:r,type:s};if(xe(t,o)&&I("absoluteBoundingBox",t)&&t.absoluteBoundingBox){let u=De(t.absoluteBoundingBox,i);l.boundingBox={x:u.x,y:u.y,width:t.absoluteBoundingBox.width,height:t.absoluteBoundingBox.height}}if(s==="INSTANCE"&&(I("componentId",t)&&(l.componentId=t.componentId),I("componentProperties",t)&&(l.componentProperties=Object.entries(t.componentProperties??{}).map(([u,{value:f,type:S}])=>({name:u,value:f.toString(),type:S})))),I("style",t)&&Object.keys(t.style).length){let u=t.style,f={fontFamily:u.fontFamily,fontWeight:u.fontWeight,fontSize:u.fontSize,lineHeight:u.lineHeightPx&&u.fontSize?`${u.lineHeightPx/u.fontSize}em`:void 0,letterSpacing:u.letterSpacing&&u.letterSpacing!==0&&u.fontSize?`${u.letterSpacing/u.fontSize*100}%`:void 0,textCase:u.textCase,textAlignHorizontal:u.textAlignHorizontal,textAlignVertical:u.textAlignVertical};l.textStyle=P(e,f,"style")}if(I("fills",t)&&Array.isArray(t.fills)&&t.fills.length){let u=t.fills.map(M);l.fills=P(e,u,"fill")}let c=ge(t);c.colors.length&&(l.strokes=P(e,c,"stroke"));let y=fe(t);Object.keys(y).length&&(l.effects=P(e,y,"effect"));let p=le(t,o);if(Object.keys(p).length>1&&(l.layout=P(e,p,"layout")),I("characters",t,Q)&&(l.text=t.characters),I("opacity",t)&&typeof t.opacity=="number"&&t.opacity!==1&&(l.opacity=t.opacity),I("cornerRadius",t)&&typeof t.cornerRadius=="number"&&(l.borderRadius=`${t.cornerRadius}px`),I("rectangleCornerRadii",t,te)&&(l.borderRadius=`${t.rectangleCornerRadii[0]}px ${t.rectangleCornerRadii[1]}px ${t.rectangleCornerRadii[2]}px ${t.rectangleCornerRadii[3]}px`),I("children",t)&&t.children.length>0){let u=t.children.filter(T).map(f=>me(e,f,t,i)).filter(f=>f!=null);u.length&&(l.children=u)}return s==="VECTOR"&&(l.type="IMAGE-SVG"),l}var a={isHTTP:!1,log:(...e)=>{a.isHTTP?console.log("[INFO]",...e):console.error("[INFO]",...e)},error:(...e)=>{console.error("[ERROR]",...e)}};import{exec as Re}from"child_process";import{promisify as Ne}from"util";var Ae=Ne(Re);async function pe(e,t={}){try{let o=await fetch(e,t);if(!o.ok)throw new Error(`Fetch failed with status ${o.status}: ${o.statusText}`);return await o.json()}catch(o){a.log(`[fetchWithRetry] Initial fetch failed for ${e}: ${o.message}. Likely a corporate proxy or SSL issue. Attempting curl fallback.`);let n=`curl -s -S --fail-with-body -L ${Fe(t.headers).join(" ")} "${e}"`;try{a.log(`[fetchWithRetry] Executing curl command: ${n}`);let{stdout:r,stderr:s}=await Ae(n);if(s){if(!r||s.toLowerCase().includes("error")||s.toLowerCase().includes("fail"))throw new Error(`Curl command failed with stderr: ${s}`);a.log(`[fetchWithRetry] Curl command for ${e} produced stderr (but might be informational): ${s}`)}if(!r)throw new Error("Curl command returned empty stdout.");return JSON.parse(r)}catch(r){throw a.error(`[fetchWithRetry] Curl fallback also failed for ${e}: ${r.message}`),o}}}function Fe(e){return e?Object.entries(e).map(([t,o])=>`-H "${t}: ${o}"`):[]}var K=class{debugInfo;constructor(){this.debugInfo={totalNodesRequested:0,pngNodesCount:0,svgNodesCount:0,imageFillsCount:0,renderRequestsCount:0,apiResponseStatus:"pending",successfulDownloads:0,failedDownloads:[],processingDetails:{validImageRefs:[],invalidImageRefs:[],excludedNodes:[]},downloadUrls:[]}}setTotalNodes(t){this.debugInfo.totalNodesRequested=t,this.logWithContext(`Total nodes requested: ${t}`)}setNodeCounts(t,o,i){this.debugInfo.pngNodesCount=t,this.debugInfo.svgNodesCount=o,this.debugInfo.imageFillsCount=i,this.debugInfo.renderRequestsCount=t+o,this.logWithContext(`Node breakdown - PNG: ${t}, SVG: ${o}, Image fills: ${i}`)}setApiResponseStatus(t){this.debugInfo.apiResponseStatus=t,this.logWithContext(`API response status: ${t}`)}addValidImageRef(t){this.debugInfo.processingDetails.validImageRefs.push(t)}addInvalidImageRef(t){this.debugInfo.processingDetails.invalidImageRefs.push(t)}addExcludedNode(t){this.debugInfo.processingDetails.excludedNodes.push(t)}addFailedDownload(t,o,i){this.debugInfo.failedDownloads.push({nodeId:t,fileName:o,reason:i}),this.logWithContext(`Failed download: ${t} (${o}) - ${i}`)}setSuccessfulDownloads(t){this.debugInfo.successfulDownloads=t,this.logWithContext(`Successful downloads: ${t}`)}addDownloadUrl(t,o,i,n,r){if(!t||!o){this.logWithContext(`Warning: Invalid parameters for addDownloadUrl - nodeId: ${t}, fileName: ${o}`);return}let s=this.debugInfo.downloadUrls.findIndex(d=>d.nodeId===t&&d.fileName===o&&d.format===i),l={nodeId:t,fileName:o,format:i,url:n,status:r};s>=0?(this.debugInfo.downloadUrls[s]=l,this.logWithContext(`Updated URL info for ${t} (${o}): ${r} - ${n?"URL available":"No URL"}`)):(this.debugInfo.downloadUrls.push(l),this.logWithContext(`Added URL info for ${t} (${o}): ${r} - ${n?"URL available":"No URL"}`))}getDownloadUrls(){return[...this.debugInfo.downloadUrls]}getDebugInfo(){return{...this.debugInfo}}generateDebugSummary(){let t=this.debugInfo,o=`Image Download Debug Summary:
`;if(o+=`\u251C\u2500 Total nodes requested: ${t.totalNodesRequested}
`,o+=`\u251C\u2500 Node breakdown:
`,o+=`\u2502 \u251C\u2500 PNG nodes: ${t.pngNodesCount}
`,o+=`\u2502 \u251C\u2500 SVG nodes: ${t.svgNodesCount}
`,o+=`\u2502 \u2514\u2500 Image fills: ${t.imageFillsCount}
`,o+=`\u251C\u2500 API response status: ${t.apiResponseStatus}
`,o+=`\u251C\u2500 Processing results:
`,o+=`\u2502 \u251C\u2500 Successful downloads: ${t.successfulDownloads}
`,o+=`\u2502 \u2514\u2500 Failed downloads: ${t.failedDownloads.length}
`,t.downloadUrls&&t.downloadUrls.length>0){let i=t.downloadUrls.reduce((n,r)=>(n.total++,n[r.status]++,n.byFormat[r.format]=(n.byFormat[r.format]||0)+1,n),{total:0,success:0,failed:0,attempted:0,byFormat:{}});o+=`\u251C\u2500 URL tracking:
`,o+=`\u2502 \u251C\u2500 Total URLs tracked: ${i.total}
`,o+=`\u2502 \u251C\u2500 Successful: ${i.success}
`,o+=`\u2502 \u251C\u2500 Failed: ${i.failed}
`,o+=`\u2502 \u251C\u2500 Attempted: ${i.attempted}
`,o+=`\u2502 \u2514\u2500 By format: PNG(${i.byFormat.PNG||0}), SVG(${i.byFormat.SVG||0}), IMAGE_FILL(${i.byFormat.IMAGE_FILL||0})
`}return t.processingDetails.excludedNodes.length>0&&(o+=`\u251C\u2500 Excluded nodes: ${t.processingDetails.excludedNodes.join(", ")}
`),t.processingDetails.invalidImageRefs.length>0&&(o+=`\u251C\u2500 Invalid image refs: ${t.processingDetails.invalidImageRefs.join(", ")}
`),t.failedDownloads.length>0?(o+=`\u2514\u2500 Failed download details:
`,t.failedDownloads.forEach((i,n)=>{let s=n===t.failedDownloads.length-1?" \u2514\u2500":" \u251C\u2500";o+=`${s} ${i.nodeId} (${i.fileName}): ${i.reason}
`})):o+=`\u2514\u2500 No failed downloads
`,o}logDebugSummary(){let t=this.generateDebugSummary();a.log(t)}logWithContext(t){a.log(`[ImageDownloadDebug] ${t}`)}};function k(){return new K}function Ce(e,t={maxLength:80,showDomain:!0,truncateIndicator:" [truncated]"}){if(!e||e.length<=t.maxLength)return e;try{let o=new URL(e),i=o.hostname;if(t.showDomain&&i){let r=`${o.protocol}//${i}`,s=t.maxLength-r.length-t.truncateIndicator.length;if(s>10){let l=o.pathname+o.search+o.hash,d=l.length>s?l.substring(0,s-3)+"...":l;return`${r}${d}${t.truncateIndicator}`}}let n=t.maxLength-t.truncateIndicator.length;return e.substring(0,n)+t.truncateIndicator}catch{let i=t.maxLength-t.truncateIndicator.length;return e.substring(0,i)+t.truncateIndicator}}function he(e){let t=`Debug Information:
`;if(t+=`\u2022 Total nodes requested: ${e.totalNodesRequested}
`,t+=`\u2022 Breakdown: ${e.pngNodesCount} PNG, ${e.svgNodesCount} SVG, ${e.imageFillsCount} image fills
`,t+=`\u2022 Successful downloads: ${e.successfulDownloads}
`,t+=`\u2022 Failed downloads: ${e.failedDownloads.length}
`,e.processingDetails.excludedNodes.length>0&&(t+=`\u2022 Excluded nodes (empty imageRef): ${e.processingDetails.excludedNodes.join(", ")}
`),e.processingDetails.invalidImageRefs.length>0&&(t+=`\u2022 Invalid image references: ${e.processingDetails.invalidImageRefs.join(", ")}
`),e.failedDownloads.length>0&&(t+=`\u2022 Failed download reasons:
`,e.failedDownloads.forEach(o=>{t+=` - ${o.nodeId}: ${o.reason}
`})),e.downloadUrls&&e.downloadUrls.length>0){t+=`
Download URLs:
`;let o=e.downloadUrls.reduce((r,s)=>(r[s.format]||(r[s.format]=[]),r[s.format].push(s),r),{}),i=["PNG","SVG","IMAGE_FILL"],n={PNG:"PNG Images",SVG:"SVG Images",IMAGE_FILL:"Image Fills"};i.forEach(r=>{let s=o[r];s&&s.length>0&&(t+=`\u2022 ${n[r]}:
`,s.forEach(l=>{let d=l.url?Ce(l.url,{maxLength:80,showDomain:!0,truncateIndicator:" [truncated]"}):"No URL available",c=l.status==="success"?"\u2713":l.status==="failed"?"\u2717":"\u25CB";t+=` ${c} ${l.nodeId} (${l.fileName}): ${d}
`}))})}return t}import Ee from"js-yaml";var _=class{apiKey;oauthToken;useOAuth;baseUrl="https://api.figma.com/v1";constructor({figmaApiKey:t,figmaOAuthToken:o,useOAuth:i}){this.apiKey=t||"",this.oauthToken=o||"",this.useOAuth=!!i&&!!this.oauthToken}async request(t){try{a.log(`Calling ${this.baseUrl}${t}`);let o={};return this.useOAuth?(a.log("Using OAuth Bearer token for authentication"),o.Authorization=`Bearer ${this.oauthToken}`):(a.log("Using Personal Access Token for authentication"),o["X-Figma-Token"]=this.apiKey),await pe(`${this.baseUrl}${t}`,{headers:o})}catch(o){throw o instanceof Error?new Error(`Failed to make request to Figma API: ${o.message}`):new Error(`Failed to make request to Figma API: ${o}`)}}async getImageFills(t,o,i,n){let r=n||k();if(a.log(`Starting image fills download process for file ${t}`),r.setTotalNodes(o.length),r.setNodeCounts(0,0,o.length),o.length===0)return a.log("No image fill nodes to process, returning empty array"),[];let s=o.map(({imageRef:h})=>h);a.log(`Image references to fetch: ${s.join(", ")}`);let l=[],d=`/files/${t}/images`;a.log(`Calling Figma API endpoint: ${this.baseUrl}${d}`);let c=await this.request(d),{images:y={}}=c.meta;a.log(`Image fills API response received with ${Object.keys(y).length} image URLs`),r.setApiResponseStatus(`Image fills API responded with ${Object.keys(y).length} URLs`);let p=[],u=[];s.forEach(h=>{y[h]?(p.push(h),r.addValidImageRef(h)):(u.push(h),r.addInvalidImageRef(h))}),p.length>0&&a.log(`Valid image references found: ${p.join(", ")}`),u.length>0&&a.log(`Invalid/missing image references: ${u.join(", ")}`),l=o.map(async({imageRef:h,fileName:b})=>{let A=y[h];return A?(a.log(`Queuing image fill download: ${h} -> ${b}`),r.addDownloadUrl(h,b,"IMAGE_FILL",A,"attempted"),Z(b,i,A,r,h,"IMAGE_FILL")):(a.log(`No image URL found for imageRef ${h} (${b})`),r.addFailedDownload("unknown",b,`Invalid imageRef: ${h}`),r.addDownloadUrl(h,b,"IMAGE_FILL",null,"failed"),"")}),a.log(`Attempting to download ${p.length} image fills out of ${o.length} requested`);let f=await Promise.all(l),S=f.filter(h=>h&&h.length>0);return r.setSuccessfulDownloads(S.length),n||r.logDebugSummary(),f}async getImages(t,o,i,n,r,s=!1,l){let d=l||k();a.log(`Starting image download process for file ${t}`),d.setTotalNodes(o.length);let c=o.filter(({fileType:m})=>m==="png"),y=o.filter(({fileType:m})=>m==="svg"),p=c.map(({nodeId:m})=>m),u=y.map(({nodeId:m})=>m);d.setNodeCounts(c.length,y.length,0),a.log(`PNG nodes count: ${c.length} (IDs: ${p.join(", ")||"none"})`),a.log(`SVG nodes count: ${y.length} (IDs: ${u.join(", ")||"none"})`);let f=`/images/${t}?ids=${p.join(",")}&format=png&scale=${n}&use_absolute_bounds=${s}`,S=p.length>0?this.request(f).then(({images:m={}})=>{a.log(`PNG API response received for ${p.length} nodes`),c.forEach(x=>{let w=m[x.nodeId];w?d.addDownloadUrl(x.nodeId,x.fileName,"PNG",w,"attempted"):d.addDownloadUrl(x.nodeId,x.fileName,"PNG",null,"failed")});let $=p.filter(x=>!m[x]);return $.length>0&&(a.log(`PNG nodes with empty/missing URLs: ${$.join(", ")}`),$.forEach(x=>{let w=c.find(F=>F.nodeId===x);w&&d.addFailedDownload(x,w.fileName,"Empty/missing URL from Figma API")})),d.setApiResponseStatus(`PNG API responded with ${Object.keys(m).length} URLs`),m}):{},h=[`ids=${u.join(",")}`,"format=svg",`svg_outline_text=${r.outlineText}`,`svg_include_id=${r.includeId}`,`svg_simplify_stroke=${r.simplifyStroke}`,`use_absolute_bounds=${s}`].join("&"),b=`/images/${t}?${h}`,A=u.length>0?this.request(b).then(({images:m={}})=>{a.log(`SVG API response received for ${u.length} nodes`),y.forEach(w=>{let F=m[w.nodeId];F?d.addDownloadUrl(w.nodeId,w.fileName,"SVG",F,"attempted"):d.addDownloadUrl(w.nodeId,w.fileName,"SVG",null,"failed")});let $=u.filter(w=>!m[w]);$.length>0&&(a.log(`SVG nodes with empty/missing URLs: ${$.join(", ")}`),$.forEach(w=>{let F=y.find(g=>g.nodeId===w);F&&d.addFailedDownload(w,F.fileName,"Empty/missing URL from Figma API")}));let x=d.getDebugInfo().apiResponseStatus;return d.setApiResponseStatus(`${x}, SVG API responded with ${Object.keys(m).length} URLs`),m}):{};a.log(`Making API requests - PNG endpoint: ${p.length>0?f:"none"}`),a.log(`Making API requests - SVG endpoint: ${u.length>0?b:"none"}`);let E=await Promise.all([S,A]).then(([m,$])=>({...m,...$}));a.log(`Combined API response contains ${Object.keys(E).length} image URLs`);let R=o.map(({nodeId:m,fileName:$,fileType:x})=>{let w=E[m];return w?(a.log(`Queuing download for node ${m} -> ${$}`),Z($,i,w,d,m,x==="png"?"PNG":"SVG")):(a.log(`No image URL found for node ${m} (${$})`),d.addFailedDownload(m,$,"No image URL found in API response"),!1)}).filter(m=>!!m);a.log(`Attempting to download ${R.length} images out of ${o.length} requested`);let B=await Promise.all(R),W=B.filter(m=>m&&m.length>0);return d.setSuccessfulDownloads(W.length),l||d.logDebugSummary(),B}async getFile(t,o){try{let i=`/files/${t}${o?`?depth=${o}`:""}`;a.log(`Retrieving Figma file: ${t} (depth: ${o??"default"})`);let n=await this.request(i);a.log("Got response");let r=X(n);return H("figma-raw.yml",n),H("figma-simplified.yml",r),r}catch(i){throw console.error("Failed to get file:",i),i}}async getNode(t,o,i){let n=`/files/${t}/nodes?ids=${o}${i?`&depth=${i}`:""}`,r=await this.request(n);a.log("Got response from getNode, now parsing."),H("figma-raw.yml",r);let s=X(r);return H("figma-simplified.yml",s),s}};function H(e,t){try{if(process.env.NODE_ENV!=="development")return;let o="logs";try{O.accessSync(process.cwd(),O.constants.W_OK)}catch(i){a.log("Failed to write logs:",i);return}O.existsSync(o)||O.mkdirSync(o),O.writeFileSync(`${o}/${e}`,Ee.dump(t))}catch(o){console.debug("Failed to write logs:",o)}}var G={COLON_FORMAT:/^[a-zA-Z0-9]+:[a-zA-Z0-9]+$/,DASH_FORMAT:/^[a-zA-Z0-9]+-[a-zA-Z0-9]+$/,VALID_CHARS:/^[a-zA-Z0-9:-]+$/,HAS_SEPARATOR:/[:-]/,MULTIPLE_DASHES:/^[a-zA-Z0-9]+-[a-zA-Z0-9-]+$/};function J(e){let t=e;if(!e||typeof e!="string"){let n="NodeId cannot be empty or null";return a.log(`NodeId validation failed: ${n}`),{isValid:!1,originalId:t||"",wasConverted:!1,error:n}}let o=e.trim();if(!o){let n="NodeId cannot be empty or null";return a.log(`NodeId validation failed: ${n}`),{isValid:!1,originalId:t,wasConverted:!1,error:n}}if(!G.VALID_CHARS.test(o)){let n=`NodeId contains invalid characters. Expected format: '1234:5678' or '1234-5678', got: '${o}'`;return a.log(`NodeId validation failed: ${n}`),{isValid:!1,originalId:t,wasConverted:!1,error:n}}if(!G.HAS_SEPARATOR.test(o)){let n=`NodeId missing separator. Expected format: '1234:5678' or '1234-5678', got: '${o}'`;return a.log(`NodeId validation failed: ${n}`),{isValid:!1,originalId:t,wasConverted:!1,error:n}}if(o.includes(":")&&o.includes("-")){a.log(`NodeId contains both dash and colon separators, prioritizing colon format: '${o}'`);let n=o.indexOf(":"),r=o.substring(0,n),s=o.substring(n+1);if(/^[a-zA-Z0-9]+$/.test(r)&&/^[a-zA-Z0-9-]+$/.test(s))return{isValid:!0,normalizedId:o,originalId:t,wasConverted:!1};{let l=`NodeId has mixed format but is not valid. Expected format: '1234:5678' or '1234-5678', got: '${o}'`;return a.log(`NodeId validation failed: ${l}`),{isValid:!1,originalId:t,wasConverted:!1,error:l}}}if(G.COLON_FORMAT.test(o))return{isValid:!0,normalizedId:o,originalId:t,wasConverted:!1};if(G.DASH_FORMAT.test(o)){let n=o.replace("-",":");return a.log(`NodeId converted from dash to colon format: '${o}' -> '${n}'`),{isValid:!0,normalizedId:n,originalId:t,wasConverted:!0}}if(G.MULTIPLE_DASHES.test(o)){let n=o.replace("-",":");return a.log(`NodeId converted from dash to colon format: '${o}' -> '${n}'`),{isValid:!0,normalizedId:n,originalId:t,wasConverted:!0}}let i=`NodeId format is invalid. Expected format: '1234:5678' or '1234-5678', got: '${o}'`;return a.log(`NodeId validation failed: ${i}`),{isValid:!1,originalId:t,wasConverted:!1,error:i}}import Te from"js-yaml";var Le={name:"Figma MCP Server111",version:"1.4.3"};function Ct(e,{isHTTP:t=!1,outputFormat:o="yaml"}={}){let i=new ve(Le),n=new _(e);return Pe(i,n,o),a.isHTTP=t,i}function Pe(e,t,o){e.tool("get_figma_data","When the nodeId cannot be obtained, obtain the layout information about the entire Figma file",{fileKey:D.string().describe("The key of the Figma file to fetch, often found in a provided URL like figma.com/(file|design)/<fileKey>/..."),nodeId:D.string().optional().describe("The ID of the node to fetch, often found as URL parameter node-id=<nodeId>. Supports both dash format (1234-5678) and colon format (1234:5678). Always use if provided."),savePath:D.string().optional().describe("The absolute path to the directory where images are stored in the project. If the directory does not exist, it will be created. The format of this path should respect the directory format of the operating system you are running on. Don't use any special character escaping in the path name either."),depth:D.number().optional().describe("OPTIONAL. Do NOT use unless explicitly requested by the user. Controls how many levels deep to traverse the node tree,")},async({fileKey:i,nodeId:n,savePath:r,depth:s})=>{try{let l=n;if(n){let b=J(n);b.isValid?(b.wasConverted&&a.log(`NodeId converted: '${n}' -> '${b.normalizedId}'`),l=b.normalizedId||n):a.log(`Warning: Invalid nodeId format '${n}': ${b.error}`)}a.log(`Fetching ${s?`${s} layers deep`:"all layers"} of ${l?`node ${l} from file`:"full file"} ${i}`);let d;l?d=await t.getNode(i,l,s):d=await t.getFile(i,s),a.log(`Successfully fetched file: ${d.name}`);let{nodes:c,globalVars:y,...p}=d,u={metadata:p,nodes:c,globalVars:y};a.log(`Generating ${o.toUpperCase()} result from file`);let f=o==="json"?JSON.stringify(u,null,2):Te.dump(u),S;if(r)try{a.log(`Saving Figma data to: ${r}`),S=await ie(u,r,i,n),a.log(`Successfully saved Figma data to: ${S}`)}catch(b){let A=b instanceof Error?b.message:String(b);a.error(`Failed to save Figma data: ${A}`)}return a.log("Sending result to client"),{content:[{type:"text",text:S?`${f}
--- FILE SAVED ---
Data saved to: ${S}`:f}]}}catch(l){let d=l instanceof Error?l.message:JSON.stringify(l);return a.error(`Error fetching file ${i}:`,d),{isError:!0,content:[{type:"text",text:r?`Error fetching file: ${d}. Note: Data could not be saved to ${r} due to the fetch error.`:`Error fetching file: ${d}`}]}}}),e.tool("download_figma_images","Download SVG and PNG images used in a Figma file based on the IDs of image or icon nodes",{fileKey:D.string().describe("The key of the Figma file containing the node"),nodes:D.object({nodeId:D.string().describe("The ID of the Figma image node to fetch. Supports both dash format (1234-5678) and colon format (1234:5678)."),imageRef:D.string().optional().describe("If a node has an imageRef fill, you must include this variable. Leave blank when downloading Vector SVG images."),fileName:D.string().describe("The local name for saving the fetched file")}).array().describe("The nodes to fetch as images"),pngScale:D.number().positive().optional().default(2).describe("Export scale for PNG images. Optional, defaults to 3 if not specified. Affects PNG images only."),localPath:D.string().describe("The absolute path to the directory where images are stored in the project. If the directory does not exist, it will be created. The format of this path should respect the directory format of the operating system you are running on. Don't use any special character escaping in the path name either."),svgOptions:D.object({outlineText:D.boolean().optional().default(!0).describe("Whether to outline text in SVG exports. Default is true."),includeId:D.boolean().optional().default(!1).describe("Whether to include IDs in SVG exports. Default is false."),simplifyStroke:D.boolean().optional().default(!0).describe("Whether to simplify strokes in SVG exports. Default is true.")}).optional().default({}).describe("Options for SVG export"),useAbsoluteBounds:D.boolean().optional().default(!1).describe("Whether to use absolute bounds for image exports. When true, includes only the element's exact boundary without extra padding. Default is false.")},async({fileKey:i,nodes:n,localPath:r,svgOptions:s,pngScale:l,useAbsoluteBounds:d})=>{try{let c=k();a.log(`Image download tool called with ${n.length} nodes for file ${i}`),c.setTotalNodes(n.length);let y=n.map(g=>{let N=J(g.nodeId);return N.isValid?(N.wasConverted&&a.log(`NodeId converted: '${g.nodeId}' -> '${N.normalizedId}'`),{...g,nodeId:N.normalizedId||g.nodeId}):(a.log(`Warning: Invalid nodeId format '${g.nodeId}': ${N.error}`),g)}),p=y.filter(({imageRef:g})=>!!g),u=y.filter(({imageRef:g})=>!g).map(({nodeId:g,fileName:N})=>({nodeId:g,fileName:N,fileType:N.endsWith(".svg")?"svg":"png"})),f=u.filter(g=>g.fileType==="png"),S=u.filter(g=>g.fileType==="svg");c.setNodeCounts(f.length,S.length,p.length),a.log(`Breakdown: ${p.length} image fills, ${u.length} render requests`),p.length>0&&a.log(`Image fill nodes: ${p.map(g=>`${g.nodeId}(${g.imageRef})`).join(", ")}`),u.length>0&&a.log(`Render request nodes: ${u.map(g=>`${g.nodeId}(${g.fileType})`).join(", ")}`);let h=n.filter(({imageRef:g})=>g==="");h.length>0&&(a.log(`Nodes excluded due to empty imageRef: ${h.map(g=>g.nodeId).join(", ")}`),h.forEach(g=>c.addExcludedNode(g.nodeId)));let b=t.getImageFills(i,p,r,c),A=t.getImages(i,u,r,l,s,d,c),E=await Promise.all([b,A]).then(([g,N])=>[...g,...N]),R=E.filter(g=>g&&g.length>0),B=E.filter(g=>!g||g.length===0);c.setSuccessfulDownloads(R.length),a.log(`Download results: ${R.length} successful, ${B.length} failed`);let W=c.getDebugInfo(),m=he(W);if(R.length===0)return c.logDebugSummary(),{content:[{type:"text",text:`Success, 0 images downloaded.
${m}`}]};let $=!E.find(g=>!g),x=$?`Success, ${R.length} images downloaded: ${R.join(", ")}`:`Partial success, ${R.length} of ${E.length} images downloaded: ${R.join(", ")}`;return{content:[{type:"text",text:!$||R.length<n.length?`${x}
${m}`:x}]}}catch(c){return a.error(`Error downloading images from file ${i}:`,c),{isError:!0,content:[{type:"text",text:`Error downloading images: ${c}`}]}}})}export{a,Ct as b};