Skip to main content
Glama
mhmzdev

Figma to Flutter MCP Server

by mhmzdev
server.ts9.69 kB
import { randomUUID } from "node:crypto"; import express, { type Request, type Response } from "express"; import { Server } from "http"; import cors from "cors"; import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js"; import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import {registerAllTools} from "./tools/index.js"; import { Logger } from "./utils/logger.js"; export function createServer(figmaApiKey: string) { const server = new McpServer({ name: "figma-flutter-mcp", version: process.env.npm_package_version || "0.0.1" }); registerAllTools(server, figmaApiKey); return server; } // Create a server instance that can handle per-user API keys export function createServerForUser(figmaApiKey: string) { return createServer(figmaApiKey); } let httpServer: Server | null = null; const transports = { streamable: {} as Record<string, StreamableHTTPServerTransport>, }; // Store MCP server instances per session (for per-user API keys) const sessionServers = {} as Record<string, McpServer>; // Helper function to extract Figma API key from request function extractFigmaApiKey(req: Request, fallbackApiKey?: string): string | null { // Try to get from Authorization header (Bearer token) const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.substring(7); } // Try to get from custom header const figmaApiKey = req.headers['x-figma-api-key'] as string; if (figmaApiKey) { return figmaApiKey; } // Try to get from query parameter (less secure, but convenient for testing) const queryApiKey = req.query.figmaApiKey as string; if (queryApiKey) { return queryApiKey; } // Fall back to server-wide API key (only for non-remote HTTP mode) return fallbackApiKey || null; } export async function startMcpServer(figmaApiKey: string): Promise<void> { try { const server = createServer(figmaApiKey); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Figma-to-Flutter MCP Server connected via stdio"); } catch (error) { console.error("Failed to start MCP server:", error); process.exit(1); } } export async function startHttpServer(port: number, figmaApiKey?: string): Promise<void> { // For remote mode, we don't create a single server instance // Instead, we create per-user servers based on their API keys // For non-remote HTTP mode, we use the provided API key const app = express(); // Configure CORS to expose Mcp-Session-Id header for browser-based clients app.use(cors({ origin: '*', // Allow all origins - adjust as needed for production exposedHeaders: ['Mcp-Session-Id'] })); // Parse JSON requests for the Streamable HTTP endpoint only, will break SSE endpoint app.use("/mcp", express.json()); // Modern Streamable HTTP endpoint app.post("/mcp", async (req, res) => { Logger.log("Received StreamableHTTP request"); const sessionId = req.headers["mcp-session-id"] as string | undefined; // Extract Figma API key from request const userFigmaApiKey = extractFigmaApiKey(req, figmaApiKey); if (!userFigmaApiKey) { res.status(401).json({ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized: Figma API key required. You must provide your own Figma API key via Authorization header (Bearer token), X-Figma-Api-Key header, or figmaApiKey query parameter. Get your API key from: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens", }, id: null, }); return; } let transport: StreamableHTTPServerTransport; let mcpServer: McpServer; if (sessionId && transports.streamable[sessionId]) { // Reuse existing transport and server Logger.log("Reusing existing StreamableHTTP transport for sessionId", sessionId); transport = transports.streamable[sessionId]; mcpServer = sessionServers[sessionId]; } else if (isInitializeRequest(req.body)) { Logger.log("New initialization request for StreamableHTTP"); // Create new server instance for this user's API key mcpServer = createServerForUser(userFigmaApiKey); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), enableJsonResponse: true, // Enable JSON response mode for better remote compatibility onsessioninitialized: (newSessionId) => { // Store the transport and server by session ID transports.streamable[newSessionId] = transport; sessionServers[newSessionId] = mcpServer; Logger.log("Session initialized with ID:", newSessionId); }, }); transport.onclose = () => { if (transport.sessionId) { delete transports.streamable[transport.sessionId]; delete sessionServers[transport.sessionId]; } }; await mcpServer.connect(transport); } else if (sessionId) { // Session ID provided but transport not found - create new one Logger.log("Creating new transport for existing sessionId", sessionId); // Create new server instance for this user's API key mcpServer = createServerForUser(userFigmaApiKey); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => sessionId, enableJsonResponse: true, // Enable JSON response mode for better remote compatibility onsessioninitialized: (newSessionId) => { transports.streamable[newSessionId] = transport; sessionServers[newSessionId] = mcpServer; }, }); transport.onclose = () => { if (transport.sessionId) { delete transports.streamable[transport.sessionId]; delete sessionServers[transport.sessionId]; } }; await mcpServer.connect(transport); } else { // Invalid request Logger.log("Invalid request:", req.body); res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: No valid session ID provided", }, id: null, }); return; } let progressInterval: NodeJS.Timeout | null = null; const progressToken = req.body.params?._meta?.progressToken; let progress = 0; if (progressToken) { Logger.log( `Setting up progress notifications for token ${progressToken} on session ${sessionId}`, ); progressInterval = setInterval(async () => { Logger.log("Sending progress notification", progress); await mcpServer.server.notification({ method: "notifications/progress", params: { progress, progressToken, }, }); progress++; }, 1000); } Logger.log("Handling StreamableHTTP request"); await transport.handleRequest(req, res, req.body); if (progressInterval) { clearInterval(progressInterval); } Logger.log("StreamableHTTP request handled"); }); // Handle GET requests for SSE streams (using built-in support from StreamableHTTP) const handleSessionRequest = async (req: Request, res: Response) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; if (!sessionId || !transports.streamable[sessionId]) { res.status(400).send("Invalid or missing session ID"); return; } console.log(`Received session termination request for session ${sessionId}`); try { const transport = transports.streamable[sessionId]; await transport.handleRequest(req, res); } catch (error) { console.error("Error handling session termination:", error); if (!res.headersSent) { res.status(500).send("Error processing session termination"); } } }; // Handle GET requests for server-to-client notifications via SSE app.get("/mcp", handleSessionRequest); // Handle DELETE requests for session termination app.delete("/mcp", handleSessionRequest); httpServer = app.listen(port, () => { Logger.log(`HTTP server listening on port ${port}`); Logger.log(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`); }); process.on("SIGINT", async () => { Logger.log("Shutting down server..."); // Close all active transports to properly clean up resources await closeTransports(transports.streamable); Logger.log("Server shutdown complete"); process.exit(0); }); } async function closeTransports( transports: Record<string, StreamableHTTPServerTransport>, ) { for (const sessionId in transports) { try { await transports[sessionId]?.close(); delete transports[sessionId]; } catch (error) { console.error(`Error closing transport for session ${sessionId}:`, error); } } } export async function stopHttpServer(): Promise<void> { if (!httpServer) { throw new Error("HTTP server is not running"); } return new Promise((resolve, reject) => { httpServer!.close((err: Error | undefined) => { if (err) { reject(err); return; } httpServer = null; const closing = Object.values(transports.streamable).map((transport) => { return transport.close(); }); Promise.all(closing).then(() => { resolve(); }); }); }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mhmzdev/figma-flutter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server