@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/src/server.ts ADDED
@@ -0,0 +1,288 @@
1
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
+ import express, { type Request, type Response } from "express";
5
+ import type { IncomingMessage, ServerResponse } from "node:http";
6
+ import { z } from "zod";
7
+ import { type FigmaImage, FigmaService } from "./figma";
8
+ import type { Logger } from "./logger";
9
+ import { ConsoleLogger, NoOpLogger, createMcpLogger } from "./logger";
10
+
11
+ // Constants
12
+ const SERVER_INFO = {
13
+ name: "SEED Figma MCP Server",
14
+ version: "0.0.1",
15
+ };
16
+
17
+ // Tool schemas
18
+ const getFigmaCodegenSchema = {
19
+ fileKey: z
20
+ .string()
21
+ .describe(
22
+ "The key of the Figma file to fetch, often found in a provided URL like figma.com/(file|design)/<fileKey>/...",
23
+ ),
24
+ nodeId: z
25
+ .string()
26
+ .regex(/^\d+:\d+$/, "Node ID must be in format integer:integer")
27
+ .describe(
28
+ "The ID of the node to fetch, formatted as 1234:5678. Convert 1234-5678 to 1234:5678",
29
+ ),
30
+ depth: z
31
+ .number()
32
+ .optional()
33
+ .describe(
34
+ "How many levels deep to traverse the node tree, only use if explicitly requested by the user",
35
+ ),
36
+ };
37
+
38
+ const fetchFigmaImagesSchema = {
39
+ fileKey: z.string().describe("The key of the Figma file containing the node"),
40
+ nodes: z
41
+ .object({
42
+ nodeId: z
43
+ .string()
44
+ .regex(/^\d+:\d+$/, "Node ID must be in format integer:integer")
45
+ .describe(
46
+ "The ID of the Figma image node to fetch, formatted as 1234:5678. Convert 1234-5678 to 1234:5678",
47
+ ),
48
+ name: z.string().describe("The name of the fetched file"),
49
+ })
50
+ .array()
51
+ .describe("The nodes to fetch as images"),
52
+ };
53
+
54
+ export class FigmaMcpServer {
55
+ private readonly server: McpServer;
56
+ private figmaService: FigmaService;
57
+ private sseTransport: SSEServerTransport | null = null;
58
+ private logger: Logger = NoOpLogger;
59
+ private readonly figmaApiKey: string;
60
+
61
+ private images = new Map<string, Omit<FigmaImage, "blob"> & { base64: string }>();
62
+
63
+ /**
64
+ * Creates a new Figma MCP Server
65
+ */
66
+ constructor(figmaApiKey: string) {
67
+ this.figmaApiKey = figmaApiKey;
68
+ this.figmaService = new FigmaService({
69
+ apiKey: figmaApiKey,
70
+ logger: this.logger,
71
+ });
72
+
73
+ this.server = new McpServer(SERVER_INFO, {
74
+ capabilities: {
75
+ logging: {},
76
+ tools: {},
77
+ resources: {},
78
+ },
79
+ });
80
+
81
+ this.registerTools();
82
+ this.registerImageResources();
83
+ }
84
+
85
+ /**
86
+ * Registers all available tools with the MCP server
87
+ */
88
+ private registerTools(): void {
89
+ this.registerGetFigmaCodegenTool();
90
+ this.registerFetchFigmaImagesTool();
91
+ }
92
+
93
+ /**
94
+ * Registers the get_figma_codegen tool
95
+ */
96
+ private registerGetFigmaCodegenTool(): void {
97
+ this.server.tool(
98
+ "get_figma_codegen",
99
+ "Fetch the generated code of a Figma file node",
100
+ getFigmaCodegenSchema,
101
+ async ({ fileKey, nodeId, depth }) => {
102
+ try {
103
+ this.logger.log(
104
+ `Fetching ${
105
+ depth ? `${depth} layers deep` : "all layers"
106
+ } of node ${nodeId} from file ${fileKey} at depth: ${depth ?? "all layers"}`,
107
+ );
108
+
109
+ const file = await this.figmaService.getGeneratedCode(fileKey, nodeId, depth);
110
+
111
+ this.logger.log(`Successfully fetched file: ${file.name}`);
112
+
113
+ return {
114
+ content: [{ type: "text", text: JSON.stringify(file) }],
115
+ };
116
+ } catch (error) {
117
+ this.logger.error(`Error fetching file ${fileKey}:`, error);
118
+ return {
119
+ content: [{ type: "text", text: `Error fetching file: ${error}` }],
120
+ };
121
+ }
122
+ },
123
+ );
124
+ }
125
+
126
+ /**
127
+ * Registers the fetch_figma_images tool
128
+ */
129
+ private registerFetchFigmaImagesTool(): void {
130
+ this.server.tool(
131
+ "fetch_figma_images",
132
+ "Fetch SVG and PNG images used in a Figma file based on the IDs",
133
+ fetchFigmaImagesSchema,
134
+ async ({ fileKey, nodes }) => {
135
+ try {
136
+ const renderRequests = nodes.map(({ nodeId, name }) => ({
137
+ nodeId,
138
+ name,
139
+ fileType: name.endsWith(".svg") ? ("svg" as const) : ("png" as const),
140
+ }));
141
+
142
+ const images = await this.figmaService.getImages(fileKey, renderRequests);
143
+
144
+ const imageCount = images.length;
145
+ const successMessage = `Success, ${imageCount} images fetched: ${images.map((image) => image.nodeId).join(", ")}`;
146
+
147
+ images.forEach((image) => {
148
+ this.images.set(image.nodeId, {
149
+ name: image.name,
150
+ fileType: image.fileType,
151
+ nodeId: image.nodeId,
152
+ base64: image.blob.toString("base64"),
153
+ });
154
+ });
155
+
156
+ // Notify clients that the resources list has changed
157
+ this.server.server.notification({
158
+ method: "notifications/resources/list_changed",
159
+ });
160
+
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: imageCount > 0 ? successMessage : "No images were fetched",
166
+ },
167
+ ],
168
+ };
169
+ } catch (error) {
170
+ this.logger.error(`Error fetching images from file ${fileKey}:`, error);
171
+ return {
172
+ content: [{ type: "text", text: `Error fetching images: ${error}` }],
173
+ };
174
+ }
175
+ },
176
+ );
177
+ }
178
+
179
+ /**
180
+ * Registers resources for Figma images
181
+ */
182
+ private registerImageResources(): void {
183
+ // Register resource template for all Figma images
184
+ this.server.resource(
185
+ "figma_images",
186
+ new ResourceTemplate("image://{nodeId}", {
187
+ list: async () => {
188
+ return {
189
+ resources: Array.from(this.images.entries()).map(([nodeId, image]) => ({
190
+ name: `Figma Image ${nodeId}`,
191
+ uri: `image://${nodeId}`,
192
+ mimeType: image.fileType === "svg" ? "image/svg+xml" : "image/png",
193
+ description: `Figma image with ID ${nodeId} (${image.fileType})`,
194
+ })),
195
+ };
196
+ },
197
+ }),
198
+ async (uri, variables) => {
199
+ const nodeId = variables["nodeId"] as string;
200
+ const image = this.images.get(nodeId);
201
+
202
+ if (!image) {
203
+ throw new Error(`Image not found for nodeId: ${nodeId}`);
204
+ }
205
+
206
+ return {
207
+ contents: [
208
+ {
209
+ uri: uri.href,
210
+ mimeType: image.fileType === "svg" ? "image/svg+xml" : "image/png",
211
+ blob: image.base64,
212
+ },
213
+ ],
214
+ };
215
+ },
216
+ );
217
+ }
218
+
219
+ /**
220
+ * Updates the logger and recreates the FigmaService with the new logger
221
+ */
222
+ private updateLogger(newLogger: Logger): void {
223
+ this.logger = newLogger;
224
+ this.figmaService = new FigmaService({
225
+ apiKey: this.figmaApiKey,
226
+ logger: this.logger,
227
+ });
228
+ }
229
+
230
+ /**
231
+ * Connects the server to a transport
232
+ */
233
+ async connect(transport: Transport): Promise<void> {
234
+ await this.server.connect(transport);
235
+
236
+ // Set up logging to the MCP client
237
+ const mcpLogger = createMcpLogger((message) => {
238
+ this.server.server.sendLoggingMessage(message);
239
+ });
240
+
241
+ this.updateLogger(mcpLogger);
242
+ this.logger.log("Server connected and ready to process requests");
243
+ }
244
+
245
+ /**
246
+ * Starts an HTTP server to serve the MCP API
247
+ */
248
+ async startHttpServer(port: number): Promise<void> {
249
+ const app = express();
250
+
251
+ // Set up SSE endpoint for MCP communication
252
+ app.get("/sse", async (_req: Request, res: Response) => {
253
+ console.log("New SSE connection established");
254
+ this.sseTransport = new SSEServerTransport(
255
+ "/messages",
256
+ res as unknown as ServerResponse<IncomingMessage>,
257
+ );
258
+
259
+ // Work around for Bun
260
+ res.write('event: log\ndata: "dummy event for bun workaround"\n\n');
261
+
262
+ await this.server.connect(this.sseTransport);
263
+ });
264
+
265
+ // Set up message endpoint for MCP communication
266
+ app.post("/messages", async (req: Request, res: Response) => {
267
+ if (!this.sseTransport) {
268
+ res.sendStatus(400);
269
+ return;
270
+ }
271
+
272
+ await this.sseTransport.handlePostMessage(
273
+ req as unknown as IncomingMessage,
274
+ res as unknown as ServerResponse<IncomingMessage>,
275
+ );
276
+ });
277
+
278
+ // Set up console logging
279
+ this.updateLogger(ConsoleLogger);
280
+
281
+ // Start the server
282
+ app.listen(port, () => {
283
+ this.logger.log(`HTTP server listening on port ${port}`);
284
+ this.logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
285
+ this.logger.log(`Message endpoint available at http://localhost:${port}/messages`);
286
+ });
287
+ }
288
+ }