@sanjeshh/framelink 1.0.0

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/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # FrameLink
2
+
3
+ FrameLink is a local MCP server that exposes Figma design data to Claude Code. It reads the currently selected node from Figma Desktop and makes it available as structured JSON through the Model Context Protocol, enabling accurate code generation without manual description or screenshots.
4
+
5
+ All data remains local. No external network requests are made.
6
+
7
+ ---
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ Figma Desktop (plugin)
13
+ │ WebSocket ws://localhost:3333
14
+ Local Bridge (Express + ws)
15
+ │ HTTP
16
+ MCP Server (stdio)
17
+ │ MCP protocol
18
+ Claude Code
19
+ ```
20
+
21
+ The Figma plugin runs in two contexts: a main thread with access to the Figma API, and a UI thread capable of network I/O. The UI thread maintains a persistent WebSocket connection to the bridge. The bridge translates HTTP requests from the MCP server into WebSocket messages and returns the plugin's response.
22
+
23
+ ---
24
+
25
+ ## Requirements
26
+
27
+ - Node.js 18+
28
+ - Figma Desktop
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```sh
35
+ npm install -g framelink
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Configuration
41
+
42
+ Register the MCP server with Claude Code. This writes to `~/.claude.json` and persists across sessions.
43
+
44
+ ```sh
45
+ claude mcp add framelink --transport stdio -- framelink
46
+ ```
47
+
48
+ Restart VSCode after running this command.
49
+
50
+ ---
51
+
52
+ ## Setup
53
+
54
+ **Bridge**
55
+
56
+ Start the local bridge server. Keep this process running while using FrameLink.
57
+
58
+ ```sh
59
+ framelink-bridge
60
+ ```
61
+
62
+ By default the bridge listens on port `3333`. To use a different port:
63
+
64
+ ```sh
65
+ PORT=4000 framelink-bridge
66
+ ```
67
+
68
+ **Figma plugin**
69
+
70
+ 1. Open Figma Desktop
71
+ 2. Navigate to **Plugins → Development → Import plugin from manifest**
72
+ 3. Select `plugin/manifest.json` from this repository
73
+ 4. Run the plugin via **Plugins → Development → FrameLink**
74
+
75
+ Keep the plugin window open. The plugin may be minimized. When the WebSocket connection is established, the status indicator turns green.
76
+
77
+ ---
78
+
79
+ ## Usage
80
+
81
+ Select a frame or component in Figma. Claude will call the appropriate tool automatically when asked about the design.
82
+
83
+ ```
84
+ Write a React component for my current Figma selection.
85
+ ```
86
+
87
+ ```
88
+ What are the text styles used in this frame?
89
+ ```
90
+
91
+ ```
92
+ Generate CSS variables from the styles in this file.
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Tools
98
+
99
+ | Tool | Description |
100
+ |------|-------------|
101
+ | `get_selection` | Returns the currently selected node and its full subtree |
102
+ | `read_my_design` | Returns full node details optimized for code generation |
103
+ | `get_document_info` | Returns document name, page list, and active page |
104
+ | `get_node_info` | Returns a single node by ID |
105
+ | `get_nodes_info` | Returns multiple nodes by ID |
106
+ | `get_local_components` | Returns all components defined on the current page |
107
+ | `get_styles` | Returns all paint, text, effect, and grid styles |
108
+ | `get_annotations` | Returns annotations attached to a node |
109
+ | `get_instance_overrides` | Returns overrides on a component instance |
110
+ | `get_reactions` | Returns prototype interactions defined on a node |
111
+ | `scan_nodes_by_types` | Returns all nodes matching the specified types |
112
+ | `scan_text_nodes` | Returns all text nodes within a given scope |
113
+ | `connection_diagnostics` | Reports bridge and plugin connection status |
114
+ | `get_active_channels` | Lists active WebSocket channels |
115
+ | `join_channel` | Returns instructions for connecting the plugin |
116
+
117
+ ---
118
+
119
+ ## Environment Variables
120
+
121
+ | Variable | Default | Description |
122
+ |----------|---------|-------------|
123
+ | `PORT` | `3333` | Bridge server port |
124
+ | `LOG_LEVEL` | `info` | Log verbosity: `debug`, `info`, `warn`, `error` |
125
+ | `FIGMA_BRIDGE_URL` | `http://localhost:3333` | Bridge URL used by the MCP server |
126
+
127
+ ---
128
+
129
+ ## Troubleshooting
130
+
131
+ **Plugin not connecting**
132
+ Verify the plugin window is open and `framelink-bridge` is running. The bridge must be started before or after the plugin — it will reconnect automatically.
133
+
134
+ **Port conflict**
135
+ ```sh
136
+ kill -9 $(lsof -ti :3333)
137
+ ```
138
+
139
+ **Tools not visible in Claude**
140
+ Re-run `claude mcp add` and restart VSCode.
141
+
142
+ **Request timeout on large documents**
143
+ Scan operations run against the current page only. Switch to a page with fewer nodes.
144
+
145
+ ---
146
+
147
+ ## License
148
+
149
+ MIT
package/dist/bridge.js ADDED
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bridge/index.ts
4
+ import express from "express";
5
+ import { createServer } from "http";
6
+ import { WebSocketServer } from "ws";
7
+
8
+ // src/bridge/routes/figma.ts
9
+ import { Router } from "express";
10
+
11
+ // src/bridge/ws/manager.ts
12
+ import { WebSocket } from "ws";
13
+ import { v4 as uuidv4 } from "uuid";
14
+
15
+ // src/bridge/logger.ts
16
+ var R = "\x1B[0m";
17
+ var DIM = "\x1B[2m";
18
+ var BOLD = "\x1B[1m";
19
+ var GREEN = "\x1B[32m";
20
+ var YELLOW = "\x1B[33m";
21
+ var RED = "\x1B[31m";
22
+ var CYAN = "\x1B[36m";
23
+ var MAGENTA = "\x1B[35m";
24
+ var ts = () => (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
25
+ var line = (s) => process.stdout.write(s + "\n");
26
+ function printBanner(port) {
27
+ line("");
28
+ line(` ${BOLD}${MAGENTA}FrameLink${R} ${DIM}Bridge${R}`);
29
+ line("");
30
+ line(` ${DIM}http${R} ${DIM}\u2192${R} ${CYAN}http://localhost:${port}${R}`);
31
+ line(` ${DIM}ws ${R} ${DIM}\u2192${R} ${CYAN}ws://localhost:${port}${R}`);
32
+ line("");
33
+ line(` ${DIM}Waiting for Figma plugin...${R}`);
34
+ line("");
35
+ }
36
+ var logger = {
37
+ info: (msg) => line(` ${DIM}${ts()}${R} ${DIM}${msg}${R}`),
38
+ success: (msg) => line(` ${GREEN}\u2713${R} ${msg}`),
39
+ warn: (msg) => line(` ${YELLOW}\u26A0${R} ${DIM}${msg}${R}`),
40
+ error: (msg) => line(` ${RED}\u2717${R} ${msg}`),
41
+ request: (type, ms) => line(` ${DIM}\u2192${R} ${type.padEnd(26)} ${DIM}${ms}ms${R}`),
42
+ debug: (msg) => {
43
+ if (process.env.LOG_LEVEL === "debug")
44
+ line(` ${DIM}\xB7 ${msg}${R}`);
45
+ }
46
+ };
47
+
48
+ // src/bridge/ws/manager.ts
49
+ var WebSocketManager = class {
50
+ constructor() {
51
+ this.ws = null;
52
+ this.pendingRequests = /* @__PURE__ */ new Map();
53
+ }
54
+ setConnection(ws) {
55
+ this.ws = ws;
56
+ logger.success("Plugin connected");
57
+ ws.on("message", (data) => {
58
+ try {
59
+ const msg = JSON.parse(data.toString());
60
+ const requestId = msg.requestId;
61
+ if (!requestId)
62
+ return;
63
+ const pending = this.pendingRequests.get(requestId);
64
+ if (!pending)
65
+ return;
66
+ this.pendingRequests.delete(requestId);
67
+ clearTimeout(pending.timeout);
68
+ const ms = Date.now() - pending.startedAt;
69
+ logger.request(pending.type, ms);
70
+ if (msg.type === "ERROR") {
71
+ pending.reject(new Error(msg.message));
72
+ } else {
73
+ pending.resolve(msg.data);
74
+ }
75
+ } catch (error) {
76
+ logger.error(`Failed to parse message: ${error}`);
77
+ }
78
+ });
79
+ ws.on("close", () => {
80
+ logger.warn("Plugin disconnected");
81
+ this.ws = null;
82
+ this.rejectAll("WebSocket disconnected");
83
+ });
84
+ ws.on("error", (error) => {
85
+ logger.error(`Plugin error: ${error}`);
86
+ this.ws = null;
87
+ this.rejectAll("WebSocket error");
88
+ });
89
+ }
90
+ rejectAll(reason) {
91
+ for (const pending of this.pendingRequests.values()) {
92
+ clearTimeout(pending.timeout);
93
+ pending.reject(new Error(reason));
94
+ }
95
+ this.pendingRequests.clear();
96
+ }
97
+ isConnected() {
98
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
99
+ }
100
+ async request(type, params, timeout = 5e3) {
101
+ if (!this.isConnected())
102
+ throw new Error("PLUGIN_UNAVAILABLE");
103
+ const requestId = uuidv4();
104
+ const startedAt = Date.now();
105
+ this.ws.send(JSON.stringify({ type, requestId, ...params }));
106
+ return new Promise((resolve, reject) => {
107
+ const timeoutHandle = setTimeout(() => {
108
+ this.pendingRequests.delete(requestId);
109
+ logger.warn(`Timeout: ${type}`);
110
+ reject(new Error("TIMEOUT"));
111
+ }, timeout);
112
+ this.pendingRequests.set(requestId, { resolve, reject, timeout: timeoutHandle, type, startedAt });
113
+ });
114
+ }
115
+ };
116
+ var wsManager = new WebSocketManager();
117
+
118
+ // src/bridge/routes/figma.ts
119
+ var router = Router();
120
+ function pluginUnavailable(res) {
121
+ return res.status(503).json({
122
+ error: { code: "PLUGIN_UNAVAILABLE", message: "Figma plugin is not connected." }
123
+ });
124
+ }
125
+ async function pluginRequest(res, type, params, timeout) {
126
+ if (!wsManager.isConnected()) {
127
+ pluginUnavailable(res);
128
+ return null;
129
+ }
130
+ try {
131
+ return await wsManager.request(type, params, timeout);
132
+ } catch (error) {
133
+ const message = String(error);
134
+ if (message.includes("NO_SELECTION")) {
135
+ res.status(404).json({ error: { code: "NO_SELECTION", message: "No node selected in Figma." } });
136
+ } else if (message.includes("TIMEOUT")) {
137
+ res.status(504).json({ error: { code: "TIMEOUT", message: "Plugin did not respond in time." } });
138
+ } else if (message.includes("PLUGIN_UNAVAILABLE")) {
139
+ pluginUnavailable(res);
140
+ } else {
141
+ logger.error(`Plugin request error: ${error}`);
142
+ res.status(500).json({ error: { code: "ERROR", message } });
143
+ }
144
+ return null;
145
+ }
146
+ }
147
+ router.get("/selection", async (req, res) => {
148
+ const data = await pluginRequest(res, "GET_SELECTION");
149
+ if (data !== null)
150
+ res.json({ selectedNode: data });
151
+ });
152
+ router.get("/read-my-design", async (req, res) => {
153
+ const data = await pluginRequest(res, "READ_MY_DESIGN");
154
+ if (data !== null)
155
+ res.json({ node: data });
156
+ });
157
+ router.get("/document-info", async (req, res) => {
158
+ const data = await pluginRequest(res, "GET_DOCUMENT_INFO");
159
+ if (data !== null)
160
+ res.json(data);
161
+ });
162
+ router.get("/node-info/:nodeId", async (req, res) => {
163
+ const data = await pluginRequest(res, "GET_NODE_INFO", { nodeId: req.params.nodeId });
164
+ if (data !== null)
165
+ res.json({ node: data });
166
+ });
167
+ router.post("/nodes-info", async (req, res) => {
168
+ const { nodeIds } = req.body;
169
+ if (!Array.isArray(nodeIds)) {
170
+ return res.status(400).json({ error: { code: "BAD_REQUEST", message: "nodeIds must be an array" } });
171
+ }
172
+ const data = await pluginRequest(res, "GET_NODES_INFO", { nodeIds });
173
+ if (data !== null)
174
+ res.json({ nodes: data });
175
+ });
176
+ router.get("/local-components", async (req, res) => {
177
+ const data = await pluginRequest(res, "GET_LOCAL_COMPONENTS", void 0, 2e4);
178
+ if (data !== null)
179
+ res.json({ components: data });
180
+ });
181
+ router.get("/styles", async (req, res) => {
182
+ const data = await pluginRequest(res, "GET_STYLES", void 0, 2e4);
183
+ if (data !== null)
184
+ res.json(data);
185
+ });
186
+ router.get("/annotations", async (req, res) => {
187
+ const nodeId = req.query.nodeId;
188
+ const data = await pluginRequest(res, "GET_ANNOTATIONS", nodeId ? { nodeId } : void 0);
189
+ if (data !== null)
190
+ res.json({ annotations: data });
191
+ });
192
+ router.get("/instance-overrides/:nodeId", async (req, res) => {
193
+ const data = await pluginRequest(res, "GET_INSTANCE_OVERRIDES", { nodeId: req.params.nodeId });
194
+ if (data !== null)
195
+ res.json(data);
196
+ });
197
+ router.get("/reactions", async (req, res) => {
198
+ const nodeId = req.query.nodeId;
199
+ const data = await pluginRequest(res, "GET_REACTIONS", nodeId ? { nodeId } : void 0);
200
+ if (data !== null)
201
+ res.json(data);
202
+ });
203
+ router.post("/scan-nodes-by-types", async (req, res) => {
204
+ const { nodeId, types } = req.body;
205
+ if (!Array.isArray(types)) {
206
+ return res.status(400).json({ error: { code: "BAD_REQUEST", message: "types must be an array" } });
207
+ }
208
+ const data = await pluginRequest(res, "SCAN_NODES_BY_TYPES", { nodeId, types }, 2e4);
209
+ if (data !== null)
210
+ res.json({ nodes: data });
211
+ });
212
+ router.get("/scan-text-nodes", async (req, res) => {
213
+ const nodeId = req.query.nodeId;
214
+ const data = await pluginRequest(res, "SCAN_TEXT_NODES", nodeId ? { nodeId } : void 0, 2e4);
215
+ if (data !== null)
216
+ res.json({ textNodes: data });
217
+ });
218
+ var figma_default = router;
219
+
220
+ // src/bridge/routes/connection.ts
221
+ import { Router as Router2 } from "express";
222
+ var router2 = Router2();
223
+ router2.get("/connection/diagnostics", (req, res) => {
224
+ const connected = wsManager.isConnected();
225
+ res.json({
226
+ status: connected ? "connected" : "disconnected",
227
+ pluginConnected: connected,
228
+ bridgePort: process.env.PORT || 3333,
229
+ wsEndpoint: `ws://localhost:${process.env.PORT || 3333}`,
230
+ httpEndpoint: `http://localhost:${process.env.PORT || 3333}`
231
+ });
232
+ });
233
+ router2.get("/connection/channels", (req, res) => {
234
+ res.json({
235
+ channels: wsManager.isConnected() ? ["figma-plugin"] : [],
236
+ count: wsManager.isConnected() ? 1 : 0
237
+ });
238
+ });
239
+ router2.post("/connection/join", (req, res) => {
240
+ res.json({
241
+ message: "Local bridge uses a direct WebSocket connection. Open the Figma plugin to connect.",
242
+ connected: wsManager.isConnected()
243
+ });
244
+ });
245
+ var connection_default = router2;
246
+
247
+ // src/bridge/index.ts
248
+ var PORT = process.env.PORT || 3333;
249
+ var app = express();
250
+ var server = createServer(app);
251
+ var wss = new WebSocketServer({ server, path: "/" });
252
+ app.use(express.json());
253
+ app.get("/health", (req, res) => {
254
+ res.json({ status: "ok", pluginConnected: wsManager.isConnected() });
255
+ });
256
+ app.use(figma_default);
257
+ app.use(connection_default);
258
+ app.use((req, res) => {
259
+ res.status(404).json({ error: "Not found" });
260
+ });
261
+ wss.on("connection", (ws) => {
262
+ wsManager.setConnection(ws);
263
+ });
264
+ wss.on("error", (error) => {
265
+ logger.error(`WebSocket server error: ${error}`);
266
+ });
267
+ server.listen(PORT, () => {
268
+ printBanner(PORT);
269
+ });
270
+ var shutdown = () => {
271
+ process.stdout.write("\n");
272
+ logger.info("Shutting down...");
273
+ server.close(() => process.exit(0));
274
+ };
275
+ process.on("SIGINT", shutdown);
276
+ process.on("SIGTERM", shutdown);
package/dist/mcp.js ADDED
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp/server.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
+
8
+ // src/mcp/bridge-client.ts
9
+ var BRIDGE_URL = process.env.FIGMA_BRIDGE_URL || "http://localhost:3333";
10
+ var PluginUnavailableError = class extends Error {
11
+ constructor() {
12
+ super(...arguments);
13
+ this.name = "PluginUnavailableError";
14
+ }
15
+ };
16
+ var NoSelectionError = class extends Error {
17
+ constructor() {
18
+ super(...arguments);
19
+ this.name = "NoSelectionError";
20
+ }
21
+ };
22
+ var TimeoutError = class extends Error {
23
+ constructor() {
24
+ super(...arguments);
25
+ this.name = "TimeoutError";
26
+ }
27
+ };
28
+ var BridgeOfflineError = class extends Error {
29
+ constructor() {
30
+ super(...arguments);
31
+ this.name = "BridgeOfflineError";
32
+ }
33
+ };
34
+ async function get(path) {
35
+ let response;
36
+ try {
37
+ response = await fetch(`${BRIDGE_URL}${path}`);
38
+ } catch {
39
+ throw new BridgeOfflineError(`Bridge not running at ${BRIDGE_URL}`);
40
+ }
41
+ if (!response.ok) {
42
+ const body = await response.json().catch(() => ({}));
43
+ const code = body.error?.code;
44
+ const message = body.error?.message || response.statusText;
45
+ if (code === "PLUGIN_UNAVAILABLE")
46
+ throw new PluginUnavailableError(message);
47
+ if (code === "NO_SELECTION")
48
+ throw new NoSelectionError(message);
49
+ if (code === "TIMEOUT")
50
+ throw new TimeoutError(message);
51
+ throw new Error(`Bridge error: ${message}`);
52
+ }
53
+ return response.json();
54
+ }
55
+ async function post(path, body) {
56
+ let response;
57
+ try {
58
+ response = await fetch(`${BRIDGE_URL}${path}`, {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify(body)
62
+ });
63
+ } catch {
64
+ throw new BridgeOfflineError(`Bridge not running at ${BRIDGE_URL}`);
65
+ }
66
+ if (!response.ok) {
67
+ const body2 = await response.json().catch(() => ({}));
68
+ const code = body2.error?.code;
69
+ const message = body2.error?.message || response.statusText;
70
+ if (code === "PLUGIN_UNAVAILABLE")
71
+ throw new PluginUnavailableError(message);
72
+ if (code === "TIMEOUT")
73
+ throw new TimeoutError(message);
74
+ throw new Error(`Bridge error: ${message}`);
75
+ }
76
+ return response.json();
77
+ }
78
+ var pluck = (key) => (d) => d[key];
79
+ var fetchSelection = () => get("/selection");
80
+ var fetchReadMyDesign = () => get("/read-my-design").then(pluck("node"));
81
+ var fetchDocumentInfo = () => get("/document-info");
82
+ var fetchNodeInfo = (nodeId) => get(`/node-info/${encodeURIComponent(nodeId)}`).then(pluck("node"));
83
+ var fetchNodesInfo = (nodeIds) => post("/nodes-info", { nodeIds }).then(pluck("nodes"));
84
+ var fetchLocalComponents = () => get("/local-components").then(pluck("components"));
85
+ var fetchStyles = () => get("/styles");
86
+ var fetchAnnotations = (nodeId) => get(`/annotations${nodeId ? `?nodeId=${encodeURIComponent(nodeId)}` : ""}`).then(pluck("annotations"));
87
+ var fetchInstanceOverrides = (nodeId) => get(`/instance-overrides/${encodeURIComponent(nodeId)}`);
88
+ var fetchReactions = (nodeId) => get(`/reactions${nodeId ? `?nodeId=${encodeURIComponent(nodeId)}` : ""}`);
89
+ var scanNodesByTypes = (types, nodeId) => post("/scan-nodes-by-types", { types, nodeId }).then(pluck("nodes"));
90
+ var scanTextNodes = (nodeId) => get(`/scan-text-nodes${nodeId ? `?nodeId=${encodeURIComponent(nodeId)}` : ""}`).then(pluck("textNodes"));
91
+ var fetchConnectionDiagnostics = () => get("/connection/diagnostics");
92
+ var fetchActiveChannels = () => get("/connection/channels");
93
+
94
+ // src/mcp/server.ts
95
+ var server = new Server(
96
+ { name: "framelink", version: "2.0.0" },
97
+ { capabilities: { tools: {} } }
98
+ );
99
+ var TOOLS = [
100
+ // Connection tools
101
+ {
102
+ name: "connection_diagnostics",
103
+ description: "Runs diagnostics on the WebSocket connection and reports status of bridge and plugin.",
104
+ inputSchema: { type: "object", properties: {} }
105
+ },
106
+ {
107
+ name: "get_active_channels",
108
+ description: "Lists all active WebSocket channels connected to the bridge.",
109
+ inputSchema: { type: "object", properties: {} }
110
+ },
111
+ {
112
+ name: "join_channel",
113
+ description: "Returns connection instructions for joining the Figma plugin channel.",
114
+ inputSchema: { type: "object", properties: {} }
115
+ },
116
+ // Read-only tools
117
+ {
118
+ name: "get_document_info",
119
+ description: "Reads details about the current Figma document including name, ID, pages, and current page.",
120
+ inputSchema: { type: "object", properties: {} }
121
+ },
122
+ {
123
+ name: "get_selection",
124
+ description: "Reads what is currently selected in Figma. Returns the selected node subtree with layout, styles, and children.",
125
+ inputSchema: { type: "object", properties: {} }
126
+ },
127
+ {
128
+ name: "read_my_design",
129
+ description: "Reads full node details of the current selection in Figma. Use this to get the complete design tree for code generation.",
130
+ inputSchema: { type: "object", properties: {} }
131
+ },
132
+ {
133
+ name: "get_node_info",
134
+ description: "Reads info about a single node by its Figma node ID.",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ nodeId: { type: "string", description: 'The Figma node ID (e.g. "454:23657")' }
139
+ },
140
+ required: ["nodeId"]
141
+ }
142
+ },
143
+ {
144
+ name: "get_nodes_info",
145
+ description: "Reads info about multiple nodes by their Figma node IDs.",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ nodeIds: { type: "array", items: { type: "string" }, description: "Array of Figma node IDs" }
150
+ },
151
+ required: ["nodeIds"]
152
+ }
153
+ },
154
+ {
155
+ name: "get_local_components",
156
+ description: "Reads all local components defined in the Figma document.",
157
+ inputSchema: { type: "object", properties: {} }
158
+ },
159
+ {
160
+ name: "get_styles",
161
+ description: "Reads all styles (paint/color, text, effect, grid) defined in the Figma document.",
162
+ inputSchema: { type: "object", properties: {} }
163
+ },
164
+ {
165
+ name: "get_annotations",
166
+ description: "Reads annotations on a document or specific node.",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ nodeId: { type: "string", description: "Optional node ID. Defaults to current selection." }
171
+ }
172
+ }
173
+ },
174
+ {
175
+ name: "get_instance_overrides",
176
+ description: "Reads override properties from a component instance node.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ nodeId: { type: "string", description: "The node ID of a component instance" }
181
+ },
182
+ required: ["nodeId"]
183
+ }
184
+ },
185
+ {
186
+ name: "get_reactions",
187
+ description: "Reads prototype interactions and reactions from a node.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ nodeId: { type: "string", description: "Optional node ID. Defaults to current selection." }
192
+ }
193
+ }
194
+ },
195
+ {
196
+ name: "scan_nodes_by_types",
197
+ description: "Scans and returns child nodes matching specified Figma node types (e.g. FRAME, TEXT, RECTANGLE).",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ types: { type: "array", items: { type: "string" }, description: 'Node types to scan for, e.g. ["TEXT", "FRAME"]' },
202
+ nodeId: { type: "string", description: "Optional root node ID. Defaults to current page." }
203
+ },
204
+ required: ["types"]
205
+ }
206
+ },
207
+ {
208
+ name: "scan_text_nodes",
209
+ description: "Scans and returns all text nodes within a node or the current page.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ nodeId: { type: "string", description: "Optional root node ID. Defaults to current page." }
214
+ }
215
+ }
216
+ }
217
+ ];
218
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
219
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
220
+ const { name, arguments: args } = request.params;
221
+ const params = args ?? {};
222
+ const ok = (data) => ({
223
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
224
+ });
225
+ const err = (message) => ({
226
+ content: [{ type: "text", text: `Error: ${message}` }],
227
+ isError: true
228
+ });
229
+ const mapError = (error) => {
230
+ if (error instanceof NoSelectionError)
231
+ return "No node selected in Figma. Please select a frame or component.";
232
+ if (error instanceof PluginUnavailableError)
233
+ return "Figma plugin is not connected. Ensure it's running and the window is open.";
234
+ if (error instanceof TimeoutError)
235
+ return "Plugin timeout. Try selecting again or restarting the plugin.";
236
+ if (error instanceof BridgeOfflineError)
237
+ return "Bridge is not running. Start it with: npm run bridge:dev";
238
+ if (error instanceof Error)
239
+ return error.message;
240
+ return "Unknown error";
241
+ };
242
+ try {
243
+ switch (name) {
244
+ case "connection_diagnostics":
245
+ return ok(await fetchConnectionDiagnostics());
246
+ case "get_active_channels":
247
+ return ok(await fetchActiveChannels());
248
+ case "join_channel":
249
+ return ok({ message: "Open the Figma plugin (Plugins \u2192 Development \u2192 Figma \u2192 Claude Code) to connect. Keep the plugin window open." });
250
+ case "get_document_info":
251
+ return ok(await fetchDocumentInfo());
252
+ case "get_selection":
253
+ return ok((await fetchSelection()).selectedNode);
254
+ case "read_my_design":
255
+ return ok(await fetchReadMyDesign());
256
+ case "get_node_info":
257
+ return ok(await fetchNodeInfo(params.nodeId));
258
+ case "get_nodes_info":
259
+ return ok(await fetchNodesInfo(params.nodeIds));
260
+ case "get_local_components":
261
+ return ok(await fetchLocalComponents());
262
+ case "get_styles":
263
+ return ok(await fetchStyles());
264
+ case "get_annotations":
265
+ return ok(await fetchAnnotations(params.nodeId));
266
+ case "get_instance_overrides":
267
+ return ok(await fetchInstanceOverrides(params.nodeId));
268
+ case "get_reactions":
269
+ return ok(await fetchReactions(params.nodeId));
270
+ case "scan_nodes_by_types":
271
+ return ok(await scanNodesByTypes(params.types, params.nodeId));
272
+ case "scan_text_nodes":
273
+ return ok(await scanTextNodes(params.nodeId));
274
+ default:
275
+ return err(`Unknown tool: ${name}`);
276
+ }
277
+ } catch (error) {
278
+ return err(mapError(error));
279
+ }
280
+ });
281
+ async function main() {
282
+ console.error("[MCP] Starting server...");
283
+ const transport = new StdioServerTransport();
284
+ await server.connect(transport);
285
+ console.error("[MCP] Server connected");
286
+ }
287
+ console.error("[MCP] Main script loaded");
288
+ main().catch((error) => {
289
+ console.error("[MCP] Fatal error:", error);
290
+ process.exit(1);
291
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@sanjeshh/framelink",
3
+ "version": "1.0.0",
4
+ "description": "Local Figma MCP server and bridge for Claude Code — read Figma selections directly in your AI workflow",
5
+ "type": "module",
6
+ "bin": {
7
+ "framelink": "./dist/mcp.js",
8
+ "framelink-bridge": "./dist/bridge.js"
9
+ },
10
+ "files": [
11
+ "dist/"
12
+ ],
13
+ "scripts": {
14
+ "build": "node build.mjs",
15
+ "link": "npm run build && npm link",
16
+ "dev:bridge": "tsx src/bridge/index.ts",
17
+ "dev:mcp": "tsx src/mcp/server.ts",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.0.0",
22
+ "express": "^4.18.2",
23
+ "uuid": "^9.0.1",
24
+ "ws": "^8.15.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/express": "^4.17.21",
28
+ "@types/node": "^20.0.0",
29
+ "@types/uuid": "^10.0.0",
30
+ "@types/ws": "^8.5.10",
31
+ "esbuild": "^0.20.0",
32
+ "tsx": "^4.7.0",
33
+ "typescript": "^5.3.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "keywords": [
39
+ "figma",
40
+ "mcp",
41
+ "claude",
42
+ "model-context-protocol",
43
+ "figma-plugin",
44
+ "ai",
45
+ "code-generation"
46
+ ],
47
+ "license": "MIT"
48
+ }