Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions jest.config.js → jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
module.exports = {
/** @type {import('jest').Config} */
const config = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: 'tsconfig.json',
tsconfig: 'tsconfig.test.json',
}],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'^~/(.*)$': '<rootDir>/src/$1'
'^~/(.*).js$': '<rootDir>/src/$1',
'^(\\.{1,2}/.*)\\.js$': '$1',
}
};

module.exports = config;
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export function getServerConfig(isStdioMode: boolean): ServerConfig {
figmaApiKey: "",
figmaOAuthToken: "",
useOAuth: false,
useCache: process.env.USE_CACHE ? process.env.USE_CACHE === "true" : false,
};

const config: Omit<ServerConfig, "auth"> = {
Expand Down
11 changes: 11 additions & 0 deletions src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { FigmaService, type FigmaAuthOptions } from "../services/figma.js";
import { Logger } from "../utils/logger.js";
import {
downloadFigmaImagesTool,
getFigmaDataSizeTool,
getFigmaDataTool,
type DownloadImagesParams,
type GetFigmaDataParams,
type GetFigmaDataSizeParams,
} from "./tools/index.js";

const serverInfo = {
Expand Down Expand Up @@ -58,6 +60,15 @@ function registerTools(
(params: DownloadImagesParams) => downloadFigmaImagesTool.handler(params, figmaService),
);
}

// Register get_figma_data_size tool
server.tool(
getFigmaDataSizeTool.name,
getFigmaDataSizeTool.description,
getFigmaDataSizeTool.parameters,
(params: GetFigmaDataSizeParams) =>
getFigmaDataSizeTool.handler(params, figmaService, options.outputFormat),
);
}

export { createServer };
107 changes: 107 additions & 0 deletions src/mcp/tools/get-figma-data-size-tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { z } from "zod";
import type { GetFileResponse, GetFileNodesResponse } from "@figma/rest-api-spec";
import { FigmaService } from "~/services/figma.js";
import { simplifyRawFigmaObject, allExtractors } from "~/extractors/index.js";
import yaml from "js-yaml";
import { Logger, writeLogs } from "~/utils/logger.js";
import { calcStringSize } from "~/utils/calc-string-size.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";

const parameters = {
fileKey: z
.string()
.describe(
"The key of the Figma file to fetch, often found in a provided URL like figma.com/(file|design)/<fileKey>/...",
),
nodeId: z
.string()
.optional()
.describe(
"The ID of the node to fetch, often found as URL parameter node-id=<nodeId>, always use if provided",
),
depth: z
.number()
.optional()
.describe(
"OPTIONAL. Do NOT use unless explicitly requested by the user. Controls how many levels deep to traverse the node tree.",
),
};

const parametersSchema = z.object(parameters);
export type GetFigmaDataSizeParams = z.infer<typeof parametersSchema>;

// Simplified handler function
async function getFigmaDataSize(
params: GetFigmaDataSizeParams,
figmaService: FigmaService,
outputFormat: "yaml" | "json",
): Promise<CallToolResult> {
try {
const { fileKey, nodeId, depth } = params;

Logger.log(
`Fetching ${depth ? `${depth} layers deep` : "all layers"} of ${
nodeId ? `node ${nodeId} from file` : `full file`
} ${fileKey}`,
);

// Get raw Figma API response
let rawApiResponse: GetFileResponse | GetFileNodesResponse;
if (nodeId) {
rawApiResponse = await figmaService.getRawNode(fileKey, nodeId, depth);
} else {
rawApiResponse = await figmaService.getRawFile(fileKey, depth);
}

// Use unified design extraction (handles nodes + components consistently)
const simplifiedDesign = simplifyRawFigmaObject(rawApiResponse, allExtractors, {
maxDepth: depth,
});

writeLogs("figma-simplified.json", simplifiedDesign);

Logger.log(
`Successfully extracted data: ${simplifiedDesign.nodes.length} nodes, ${Object.keys(simplifiedDesign.globalVars.styles).length} styles`,
);

const { nodes, globalVars, ...metadata } = simplifiedDesign;
const result = {
metadata,
nodes,
globalVars,
};

Logger.log(`Generating ${outputFormat.toUpperCase()} result from extracted data`);
const formattedResult =
outputFormat === "json" ? JSON.stringify(result, null, 2) : yaml.dump(result);


const size = calcStringSize(formattedResult);
const sizeResult = [{
nodeId: nodeId,
size: size,
}];

Logger.log("Sending result to client");

return {
content: [{ type: "text", text: yaml.dump(sizeResult) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
Logger.error(`Error fetching file ${params.fileKey}:`, message);
return {
isError: true,
content: [{ type: "text", text: `Error fetching file: ${message}` }],
};
}
}

// Export tool configuration
export const getFigmaDataSizeTool = {
name: "get_figma_data_size",
description:
"Obtain the memory size of a figma data, return the nodeId and size in KB, e.g \n- nodeId: '1234:5678'\n size: 1024 KB",
parameters,
handler: getFigmaDataSize,
} as const;
63 changes: 58 additions & 5 deletions src/mcp/tools/get-figma-data-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { FigmaService } from "~/services/figma.js";
import { simplifyRawFigmaObject, allExtractors } from "~/extractors/index.js";
import yaml from "js-yaml";
import { Logger, writeLogs } from "~/utils/logger.js";
import { calcStringSize } from "~/utils/calc-string-size.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";

const parameters = {
fileKey: z
Expand All @@ -28,12 +30,17 @@ const parameters = {
const parametersSchema = z.object(parameters);
export type GetFigmaDataParams = z.infer<typeof parametersSchema>;

const sizeLimit = process.env.GET_NODE_SIZE_LIMIT
? parseInt(process.env.GET_NODE_SIZE_LIMIT)
: undefined;
const needLimitPrompt = sizeLimit && sizeLimit > 0;

// Simplified handler function
async function getFigmaData(
params: GetFigmaDataParams,
figmaService: FigmaService,
outputFormat: "yaml" | "json",
) {
): Promise<CallToolResult> {
try {
const { fileKey, nodeId, depth } = params;

Expand Down Expand Up @@ -73,25 +80,71 @@ async function getFigmaData(
const formattedResult =
outputFormat === "json" ? JSON.stringify(result, null, 2) : yaml.dump(result);

const size = calcStringSize(formattedResult);

const overSizeLimit = sizeLimit && size > sizeLimit;
const depthLimit = !depth || depth > 1;

Logger.log(`Data Size: ${size} KB`);

if (overSizeLimit) {
Logger.log(`Data Size over size limit: ${sizeLimit} KB`);
if (depthLimit) {
Logger.log(`send error to client, need to perform truncated reading`);
return {
isError: true,
content: [
{
type: "text",
text: `The data size of file ${fileKey} ${nodeId ? `node ${nodeId}` : ""} is ${size} KB, exceeded the limit of ${sizeLimit} KB, please performing truncated reading`,
},
],
};
}
}

Logger.log("Sending result to client");
return {
content: [{ type: "text" as const, text: formattedResult }],
content: [{ type: "text", text: formattedResult }],
};
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
Logger.error(`Error fetching file ${params.fileKey}:`, message);
return {
isError: true,
content: [{ type: "text" as const, text: `Error fetching file: ${message}` }],
content: [{ type: "text", text: `Error fetching file: ${message}` }],
};
}
}

// Export tool configuration
export const getFigmaDataTool = {
name: "get_figma_data",
description:
"Get comprehensive Figma file data including layout, content, visuals, and component information",
description: `Get comprehensive Figma file data including layout, content, visuals, and component information
${
needLimitPrompt
? `## Figma Data Size Guidelines
- **Over ${sizeLimit}KB**: with \`depth: 1\` to get structure only, enter the *Pruning Reading Strategy*
- **Under ${sizeLimit}KB**: Get full data without depth parameter

## Figma Data Pruning Reading Strategy

**IMPORTANT: Work incrementally, not comprehensively.**

### Core Principle
Retrieve and implement ONE node at a time. Don't try to understand the entire design upfront.

### Pruning Reading Process
1. **Start Small**: Get shallow data (depth: 1) of the main node to see basic information of itself and children nodes
2. **Pick One**: Choose one from the child nodes to implement completely
3. **Get Full Data**: Retrieve complete data for that one node only
4. **Implement**: Implement specifically according to user needs based on the content of the node
5. **Repeat**: Move to the next node only after the current one is done

### Key Point
**Don't analyze multiple nodes in parallel.** Focus on implementing one complete, working node at a time. This avoids context overload and produces better results.`
: ""
}`,
parameters,
handler: getFigmaData,
} as const;
2 changes: 2 additions & 0 deletions src/mcp/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { getFigmaDataTool } from "./get-figma-data-tool.js";
export { downloadFigmaImagesTool } from "./download-figma-images-tool.js";
export { getFigmaDataSizeTool } from "./get-figma-data-size-tool.js";
export type { DownloadImagesParams } from "./download-figma-images-tool.js";
export type { GetFigmaDataParams } from "./get-figma-data-tool.js";
export type { GetFigmaDataSizeParams } from "./get-figma-data-size-tool.js";
17 changes: 15 additions & 2 deletions src/services/figma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {
GetFileResponse,
GetFileNodesResponse,
GetImageFillsResponse,
GetFileMetaResponse,
} from "@figma/rest-api-spec";
import { downloadFigmaImage } from "~/utils/common.js";
import { downloadAndProcessImage, type ImageProcessingResult } from "~/utils/image-processing.js";
import { Logger, writeLogs } from "~/utils/logger.js";
import { fetchWithRetry } from "~/utils/fetch-with-retry.js";
Expand All @@ -13,6 +13,7 @@ export type FigmaAuthOptions = {
figmaApiKey: string;
figmaOAuthToken: string;
useOAuth: boolean;
useCache: boolean;
};

type SvgOptions = {
Expand All @@ -26,11 +27,13 @@ export class FigmaService {
private readonly oauthToken: string;
private readonly useOAuth: boolean;
private readonly baseUrl = "https://api.figma.com/v1";
private readonly useCache: boolean;

constructor({ figmaApiKey, figmaOAuthToken, useOAuth }: FigmaAuthOptions) {
constructor({ figmaApiKey, figmaOAuthToken, useOAuth, useCache }: FigmaAuthOptions) {
this.apiKey = figmaApiKey || "";
this.oauthToken = figmaOAuthToken || "";
this.useOAuth = !!useOAuth && !!this.oauthToken;
this.useCache = useCache;
}

private getAuthHeaders(): Record<string, string> {
Expand Down Expand Up @@ -255,6 +258,16 @@ export class FigmaService {
return results.flat();
}

/**
* Get file meta data
*/
async getFileMeta(fileKey: string): Promise<GetFileMetaResponse> {
const endpoint = `/files/${fileKey}/meta`;
const response = await this.request<GetFileMetaResponse>(endpoint);
return response;
}


/**
* Get raw Figma API response for a file (for use with flexible extractors)
*/
Expand Down
Loading