@seed-design/mcp 0.0.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/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@seed-design/mcp",
3
+ "version": "0.0.2",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/daangn/seed-design.git",
7
+ "directory": "packages/mcp"
8
+ },
9
+ "author": "Asher <asher@daangn.com>",
10
+ "license": "MIT",
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "files": [
14
+ "bin",
15
+ "src"
16
+ ],
17
+ "bin": "./bin/index.mjs",
18
+ "scripts": {
19
+ "clean": "rm -rf lib",
20
+ "build": "bunchee",
21
+ "lint:publish": "bun publint"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.7.0",
25
+ "@seed-design/figma": "0.0.2",
26
+ "express": "^4.21.2",
27
+ "yargs": "^17.7.2"
28
+ },
29
+ "devDependencies": {
30
+ "@types/yargs": "^17.0.33",
31
+ "typescript": "^5.4.5"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { startServer } from "../index";
4
+ import { ConsoleLogger } from "../logger";
5
+
6
+ /**
7
+ * Main CLI entry point
8
+ */
9
+ async function main(): Promise<void> {
10
+ try {
11
+ // Set environment to indicate CLI mode
12
+ process.env["NODE_ENV"] = "cli";
13
+
14
+ // Start server
15
+ await startServer();
16
+ } catch (error) {
17
+ handleStartupError(error);
18
+ process.exit(1);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Handles and logs startup errors
24
+ */
25
+ function handleStartupError(error: unknown): void {
26
+ if (error instanceof Error) {
27
+ ConsoleLogger.error("Failed to start server:", error.message);
28
+ } else {
29
+ ConsoleLogger.error("Failed to start server with unknown error:", error);
30
+ }
31
+ }
32
+
33
+ // Run the application
34
+ main().catch(handleStartupError);
package/src/config.ts ADDED
@@ -0,0 +1,166 @@
1
+ import yargs from "yargs";
2
+ import { hideBin } from "yargs/helpers";
3
+ import type { Logger } from "./logger";
4
+ import { NoOpLogger } from "./logger";
5
+
6
+ /**
7
+ * Configuration for the Figma MCP Server
8
+ */
9
+ export interface ServerConfig {
10
+ figmaApiKey: string;
11
+ port: number;
12
+ configSources: {
13
+ figmaApiKey: ConfigSource;
14
+ port: ConfigSource;
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Source of configuration value
20
+ */
21
+ type ConfigSource = "cli" | "env" | "default";
22
+
23
+ /**
24
+ * Command line arguments
25
+ */
26
+ interface CliArgs {
27
+ "figma-api-key"?: string;
28
+ port?: number;
29
+ }
30
+
31
+ /**
32
+ * Configuration manager for the Figma MCP Server
33
+ */
34
+ export class ConfigManager {
35
+ private readonly isStdioMode: boolean;
36
+ private readonly logger: Logger;
37
+
38
+ /**
39
+ * Creates a new ConfigManager instance
40
+ */
41
+ constructor(options: { isStdioMode: boolean; logger?: Logger }) {
42
+ this.isStdioMode = options.isStdioMode;
43
+ this.logger = options.logger || NoOpLogger;
44
+ }
45
+
46
+ /**
47
+ * Gets the server configuration from command line arguments and environment variables
48
+ */
49
+ getServerConfig(): ServerConfig {
50
+ // Parse command line arguments
51
+ const argv = this.parseCommandLineArgs();
52
+
53
+ // Initialize config with default values
54
+ const config: ServerConfig = {
55
+ figmaApiKey: "",
56
+ port: 3333,
57
+ configSources: {
58
+ figmaApiKey: "env",
59
+ port: "default",
60
+ },
61
+ };
62
+
63
+ // Handle FIGMA_API_KEY
64
+ this.configureFigmaApiKey(config, argv);
65
+
66
+ // Handle PORT
67
+ this.configurePort(config, argv);
68
+
69
+ // Validate configuration
70
+ this.validateConfig(config);
71
+
72
+ // Log configuration sources
73
+ this.logConfig(config);
74
+
75
+ return config;
76
+ }
77
+
78
+ /**
79
+ * Parses command line arguments
80
+ */
81
+ private parseCommandLineArgs(): CliArgs {
82
+ return yargs(hideBin(process.argv))
83
+ .options({
84
+ "figma-api-key": {
85
+ type: "string",
86
+ description: "Figma API key",
87
+ },
88
+ port: {
89
+ type: "number",
90
+ description: "Port to run the server on",
91
+ },
92
+ })
93
+ .help()
94
+ .parseSync() as CliArgs;
95
+ }
96
+
97
+ /**
98
+ * Configures the Figma API key
99
+ */
100
+ private configureFigmaApiKey(config: ServerConfig, argv: CliArgs): void {
101
+ if (argv["figma-api-key"]) {
102
+ config.figmaApiKey = argv["figma-api-key"];
103
+ config.configSources.figmaApiKey = "cli";
104
+ } else if (process.env["FIGMA_API_KEY"]) {
105
+ config.figmaApiKey = process.env["FIGMA_API_KEY"];
106
+ config.configSources.figmaApiKey = "env";
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Configures the server port
112
+ */
113
+ private configurePort(config: ServerConfig, argv: CliArgs): void {
114
+ if (argv.port) {
115
+ config.port = argv.port;
116
+ config.configSources.port = "cli";
117
+ } else if (process.env["PORT"]) {
118
+ config.port = Number.parseInt(process.env["PORT"], 10);
119
+ config.configSources.port = "env";
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Validates the configuration
125
+ */
126
+ private validateConfig(config: ServerConfig): void {
127
+ if (!config.figmaApiKey) {
128
+ this.logger.error(
129
+ "FIGMA_API_KEY is required (via CLI argument --figma-api-key or .env file)",
130
+ );
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Logs the configuration
137
+ */
138
+ private logConfig(config: ServerConfig): void {
139
+ if (this.isStdioMode) {
140
+ return;
141
+ }
142
+
143
+ this.logger.log("\nConfiguration:");
144
+ this.logger.log(
145
+ `- FIGMA_API_KEY: ${this.maskApiKey(config.figmaApiKey)} (source: ${config.configSources.figmaApiKey})`,
146
+ );
147
+ this.logger.log(`- PORT: ${config.port} (source: ${config.configSources.port})`);
148
+ this.logger.log(""); // Empty line for better readability
149
+ }
150
+
151
+ /**
152
+ * Masks an API key for secure logging
153
+ */
154
+ private maskApiKey(key: string): string {
155
+ if (key.length <= 4) return "****";
156
+ return `****${key.slice(-4)}`;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Gets the server configuration
162
+ */
163
+ export function getServerConfig(isStdioMode: boolean): ServerConfig {
164
+ const configManager = new ConfigManager({ isStdioMode });
165
+ return configManager.getServerConfig();
166
+ }
package/src/figma.ts ADDED
@@ -0,0 +1,211 @@
1
+ import type { GetFileNodesResponse, GetImagesResponse } from "@figma/rest-api-spec";
2
+ import fs from "fs";
3
+ import { generateCode, createRestNormalizer } from "@seed-design/figma";
4
+ import type { Logger } from "./logger";
5
+ import { NoOpLogger } from "./logger";
6
+
7
+ export interface FigmaError {
8
+ status: number;
9
+ err: string;
10
+ }
11
+
12
+ export interface FetchImageParams {
13
+ /**
14
+ * The Node in Figma that will either be rendered or have its background image downloaded
15
+ */
16
+ nodeId: string;
17
+ /**
18
+ * The file mimetype for the image
19
+ */
20
+ fileType: "png" | "svg";
21
+ /**
22
+ * The filename to save the image as
23
+ */
24
+ name: string;
25
+ }
26
+
27
+ export interface FigmaImage {
28
+ nodeId: string;
29
+ name: string;
30
+ blob: Buffer;
31
+ fileType: "png" | "svg";
32
+ }
33
+
34
+ export interface SimplifiedDesign {
35
+ name: string;
36
+ lastModified: string;
37
+ code: string;
38
+ }
39
+
40
+ interface FigmaServiceOptions {
41
+ apiKey: string;
42
+ baseUrl?: string;
43
+ logger?: Logger;
44
+ }
45
+
46
+ export class FigmaService {
47
+ private readonly apiKey: string;
48
+ private readonly baseUrl: string;
49
+ private readonly logger: Logger;
50
+
51
+ constructor(options: FigmaServiceOptions) {
52
+ this.apiKey = options.apiKey;
53
+ this.baseUrl = options.baseUrl || "https://api.figma.com/v1";
54
+ this.logger = options.logger || NoOpLogger;
55
+ }
56
+
57
+ private async request<T>(endpoint: string): Promise<T> {
58
+ try {
59
+ this.logger.log(`Calling ${this.baseUrl}${endpoint}`);
60
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
61
+ headers: {
62
+ "X-Figma-Token": this.apiKey,
63
+ },
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const errorData = (await response.json().catch(() => ({}))) as { err?: string };
68
+ throw {
69
+ status: response.status,
70
+ err: errorData.err || "Unknown error",
71
+ } as FigmaError;
72
+ }
73
+
74
+ return response.json() as Promise<T>;
75
+ } catch (error) {
76
+ if (error instanceof Error) {
77
+ throw new Error(`Failed to make request to Figma API: ${error.message}`);
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ async getImages(fileKey: string, nodes: FetchImageParams[]): Promise<FigmaImage[]> {
84
+ const pngNodes = nodes.filter(({ fileType }) => fileType === "png");
85
+ const svgNodes = nodes.filter(({ fileType }) => fileType === "svg");
86
+
87
+ const imagesMap = await this.fetchImageUrls(fileKey, pngNodes, svgNodes);
88
+
89
+ const images = await this.fetchImage(nodes, imagesMap);
90
+
91
+ return images;
92
+ }
93
+
94
+ private async fetchImageUrls(
95
+ fileKey: string,
96
+ pngNodes: FetchImageParams[],
97
+ svgNodes: FetchImageParams[],
98
+ ): Promise<Record<string, string>> {
99
+ const pngIds = pngNodes.map(({ nodeId }) => nodeId);
100
+ const pngFiles =
101
+ pngIds.length > 0
102
+ ? this.request<GetImagesResponse>(
103
+ `/images/${fileKey}?ids=${pngIds.join(",")}&scale=2&format=png`,
104
+ ).then(({ images = {} }) => images)
105
+ : ({} as GetImagesResponse["images"]);
106
+
107
+ const svgIds = svgNodes.map(({ nodeId }) => nodeId);
108
+ const svgFiles =
109
+ svgIds.length > 0
110
+ ? this.request<GetImagesResponse>(
111
+ `/images/${fileKey}?ids=${svgIds.join(",")}&format=svg`,
112
+ ).then(({ images = {} }) => images)
113
+ : ({} as GetImagesResponse["images"]);
114
+
115
+ const [pngImages, svgImages] = await Promise.all([pngFiles, svgFiles]);
116
+ const combinedImages: Record<string, string> = {};
117
+
118
+ Object.entries({ ...pngImages, ...svgImages }).forEach(([key, value]) => {
119
+ if (value !== null) {
120
+ combinedImages[key] = value;
121
+ }
122
+ });
123
+
124
+ return combinedImages;
125
+ }
126
+
127
+ private async fetchImage(
128
+ nodes: FetchImageParams[],
129
+ imagesMap: Record<string, string>,
130
+ ): Promise<FigmaImage[]> {
131
+ const fetchPromises = nodes
132
+ .map(({ nodeId, name, fileType }) => {
133
+ const imageUrl = imagesMap[nodeId];
134
+ if (imageUrl) {
135
+ return fetch(imageUrl)
136
+ .then((response) => response.arrayBuffer())
137
+ .then((arrayBuffer) => Buffer.from(arrayBuffer))
138
+ .then((buffer) => {
139
+ return {
140
+ nodeId,
141
+ name,
142
+ blob: buffer,
143
+ fileType,
144
+ };
145
+ })
146
+ .catch((error) => {
147
+ this.logger.error(`Failed to fetch image for ${nodeId}:`, error);
148
+ return undefined;
149
+ });
150
+ }
151
+ return undefined;
152
+ })
153
+ .filter((x): x is Promise<FigmaImage> => x !== undefined);
154
+
155
+ return Promise.all(fetchPromises);
156
+ }
157
+
158
+ async getGeneratedCode(
159
+ fileKey: string,
160
+ nodeId: string,
161
+ depth?: number,
162
+ ): Promise<SimplifiedDesign> {
163
+ const endpoint = `/files/${fileKey}/nodes?ids=${nodeId}${depth ? `&depth=${depth}` : ""}`;
164
+ const response = await this.request<GetFileNodesResponse>(endpoint);
165
+
166
+ this.writeDebugLogs("figma-raw.json", response);
167
+
168
+ const node = Object.values(response.nodes)[0]!;
169
+
170
+ const normalizer = createRestNormalizer({
171
+ styles: node.styles,
172
+ components: node.components,
173
+ componentSets: node.componentSets,
174
+ });
175
+
176
+ const normalizedNode = normalizer(node.document);
177
+ const code = await generateCode(normalizedNode);
178
+
179
+ const result = {
180
+ name: node.document.name,
181
+ lastModified: response.lastModified,
182
+ code,
183
+ };
184
+
185
+ this.writeDebugLogs("figma-result.json", result);
186
+ return result;
187
+ }
188
+
189
+ private writeDebugLogs(name: string, value: any): void {
190
+ try {
191
+ if (process.env["NODE_ENV"] !== "development") return;
192
+
193
+ const logsDir = "logs";
194
+
195
+ try {
196
+ fs.accessSync(process.cwd(), fs.constants.W_OK);
197
+ } catch (error) {
198
+ this.logger.error("Failed to write logs:", error);
199
+ return;
200
+ }
201
+
202
+ if (!fs.existsSync(logsDir)) {
203
+ fs.mkdirSync(logsDir);
204
+ }
205
+
206
+ fs.writeFileSync(`${logsDir}/${name}`, JSON.stringify(value, null, 2));
207
+ } catch (error) {
208
+ this.logger.error("Failed to write logs:", error);
209
+ }
210
+ }
211
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { FigmaMcpServer } from "./server";
3
+ import { getServerConfig } from "./config";
4
+ import { ConsoleLogger } from "./logger";
5
+
6
+ /**
7
+ * Determines if the server should run in stdio mode
8
+ */
9
+ function isRunningInStdioMode(): boolean {
10
+ return process.env["NODE_ENV"] === "cli" || process.argv.includes("--stdio");
11
+ }
12
+
13
+ /**
14
+ * Starts the Figma MCP server
15
+ */
16
+ export async function startServer(): Promise<void> {
17
+ // Determine server mode (stdio or HTTP)
18
+ const stdioMode = isRunningInStdioMode();
19
+
20
+ // Get configuration
21
+ const config = getServerConfig(stdioMode);
22
+
23
+ // Create server instance
24
+ const server = new FigmaMcpServer(config.figmaApiKey);
25
+
26
+ // Start in appropriate mode
27
+ if (stdioMode) {
28
+ // Connect to stdio transport for CLI mode
29
+ const transport = new StdioServerTransport();
30
+ await server.connect(transport);
31
+ } else {
32
+ // Start HTTP server for web mode
33
+ ConsoleLogger.log(`Initializing Figma MCP Server in HTTP mode on port ${config.port}...`);
34
+ await server.startHttpServer(config.port);
35
+ }
36
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Centralized logger module for the Figma MCP Server
3
+ */
4
+ export interface Logger {
5
+ log(...args: any[]): void;
6
+ error(...args: any[]): void;
7
+ }
8
+
9
+ /**
10
+ * Default logger implementation that does nothing
11
+ */
12
+ export const NoOpLogger: Logger = {
13
+ log: () => {},
14
+ error: () => {},
15
+ };
16
+
17
+ /**
18
+ * Logger that writes to the console
19
+ */
20
+ export const ConsoleLogger: Logger = {
21
+ log: console.log,
22
+ error: console.error,
23
+ };
24
+
25
+ /**
26
+ * Creates a logger that sends messages to an MCP server
27
+ */
28
+ export function createMcpLogger(sendLoggingMessage: (message: any) => void): Logger {
29
+ return {
30
+ log: (...args: any[]) => {
31
+ sendLoggingMessage({
32
+ level: "info",
33
+ data: args,
34
+ });
35
+ },
36
+ error: (...args: any[]) => {
37
+ sendLoggingMessage({
38
+ level: "error",
39
+ data: args,
40
+ });
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,16 @@
1
+ import fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+
5
+ const defaultDownloadsPath = path.join(os.homedir(), "Downloads");
6
+
7
+ export function saveImage(image: Buffer, name: string, type: "png" | "svg") {
8
+ if (!fs.existsSync(defaultDownloadsPath)) {
9
+ fs.mkdirSync(defaultDownloadsPath, { recursive: true });
10
+ }
11
+
12
+ const filePath = path.join(defaultDownloadsPath, `${name}.${type}`);
13
+ fs.writeFileSync(filePath, image);
14
+
15
+ return filePath;
16
+ }