@gethopp/figma-mcp-bridge 0.0.1

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.
@@ -0,0 +1,16 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { Duplex } from "node:stream";
3
+ import type { BridgeResponse } from "./types.js";
4
+ export declare class Bridge {
5
+ private wss;
6
+ private conn;
7
+ private pending;
8
+ private counter;
9
+ constructor();
10
+ handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;
11
+ private handleConnection;
12
+ send(requestType: string, nodeIds?: string[]): Promise<BridgeResponse>;
13
+ sendWithParams(requestType: string, nodeIds?: string[], params?: Record<string, unknown>): Promise<BridgeResponse>;
14
+ private nextId;
15
+ close(): void;
16
+ }
package/dist/bridge.js ADDED
@@ -0,0 +1,103 @@
1
+ import { WebSocketServer, WebSocket } from "ws";
2
+ export class Bridge {
3
+ wss;
4
+ conn = null;
5
+ pending = new Map();
6
+ counter = 0;
7
+ constructor() {
8
+ this.wss = new WebSocketServer({ noServer: true });
9
+ }
10
+ handleUpgrade(request, socket, head) {
11
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
12
+ this.handleConnection(ws);
13
+ });
14
+ }
15
+ handleConnection(ws) {
16
+ // Replace existing connection (same behavior as Go version)
17
+ if (this.conn) {
18
+ this.conn.close();
19
+ }
20
+ this.conn = ws;
21
+ ws.on("message", (data) => {
22
+ try {
23
+ const resp = JSON.parse(data.toString());
24
+ const pending = this.pending.get(resp.requestId);
25
+ if (pending) {
26
+ clearTimeout(pending.timeout);
27
+ this.pending.delete(resp.requestId);
28
+ pending.resolve(resp);
29
+ }
30
+ }
31
+ catch {
32
+ console.error("Invalid response from plugin");
33
+ }
34
+ });
35
+ ws.on("close", () => {
36
+ if (this.conn === ws) {
37
+ this.conn = null;
38
+ }
39
+ });
40
+ ws.on("error", (err) => {
41
+ console.error("WebSocket error:", err.message);
42
+ if (this.conn === ws) {
43
+ this.conn = null;
44
+ }
45
+ });
46
+ }
47
+ send(requestType, nodeIds) {
48
+ return this.sendWithParams(requestType, nodeIds);
49
+ }
50
+ sendWithParams(requestType, nodeIds, params) {
51
+ return new Promise((resolve, reject) => {
52
+ if (!this.conn || this.conn.readyState !== WebSocket.OPEN) {
53
+ reject(new Error("Plugin not connected"));
54
+ return;
55
+ }
56
+ const requestId = this.nextId();
57
+ const request = {
58
+ type: requestType,
59
+ requestId,
60
+ };
61
+ if (nodeIds && nodeIds.length > 0) {
62
+ request.nodeIds = nodeIds;
63
+ }
64
+ if (params && Object.keys(params).length > 0) {
65
+ request.params = params;
66
+ }
67
+ const timeout = setTimeout(() => {
68
+ this.pending.delete(requestId);
69
+ reject(new Error("Request timed out"));
70
+ }, 30_000);
71
+ this.pending.set(requestId, { resolve, reject, timeout });
72
+ this.conn.send(JSON.stringify(request), (err) => {
73
+ if (err) {
74
+ clearTimeout(timeout);
75
+ this.pending.delete(requestId);
76
+ reject(err);
77
+ }
78
+ });
79
+ });
80
+ }
81
+ nextId() {
82
+ this.counter++;
83
+ const now = new Date();
84
+ const hh = String(now.getHours()).padStart(2, "0");
85
+ const mm = String(now.getMinutes()).padStart(2, "0");
86
+ const ss = String(now.getSeconds()).padStart(2, "0");
87
+ return `req-${hh}${mm}${ss}-${this.counter}`;
88
+ }
89
+ close() {
90
+ // Reject all pending requests
91
+ for (const [id, { reject, timeout }] of this.pending) {
92
+ clearTimeout(timeout);
93
+ reject(new Error("Bridge closed"));
94
+ }
95
+ this.pending.clear();
96
+ if (this.conn) {
97
+ this.conn.close();
98
+ this.conn = null;
99
+ }
100
+ this.wss.close();
101
+ }
102
+ }
103
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAWhD,MAAM,OAAO,MAAM;IACT,GAAG,CAAkB;IACrB,IAAI,GAAqB,IAAI,CAAC;IAC9B,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAC;IAC5C,OAAO,GAAG,CAAC,CAAC;IAEpB;QACE,IAAI,CAAC,GAAG,GAAG,IAAI,eAAe,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,aAAa,CAAC,OAAwB,EAAE,MAAc,EAAE,IAAY;QAClE,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE;YACnD,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,gBAAgB,CAAC,EAAa;QACpC,4DAA4D;QAC5D,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;QAEf,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,IAAI,CAAC;gBACH,MAAM,IAAI,GAAmB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;gBACzD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACjD,IAAI,OAAO,EAAE,CAAC;oBACZ,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;oBAC9B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;oBACpC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACxB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAChD,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAClB,IAAI,IAAI,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;gBACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;YACnB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACrB,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YAC/C,IAAI,IAAI,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;gBACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;YACnB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC,WAAmB,EAAE,OAAkB;QAC1C,OAAO,IAAI,CAAC,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IAED,cAAc,CACZ,WAAmB,EACnB,OAAkB,EAClB,MAAgC;QAEhC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;gBAC1D,MAAM,CAAC,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC,CAAC;gBAC1C,OAAO;YACT,CAAC;YAED,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,OAAO,GAAkB;gBAC7B,IAAI,EAAE,WAAW;gBACjB,SAAS;aACV,CAAC;YACF,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAClC,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC;YAC5B,CAAC;YACD,IAAI,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7C,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;YAC1B,CAAC;YAED,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC9B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBAC/B,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;YACzC,CAAC,EAAE,MAAM,CAAC,CAAC;YAEX,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;YAE1D,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE;gBAC9C,IAAI,GAAG,EAAE,CAAC;oBACR,YAAY,CAAC,OAAO,CAAC,CAAC;oBACtB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;oBAC/B,MAAM,CAAC,GAAG,CAAC,CAAC;gBACd,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACnD,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACrD,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACrD,OAAO,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;IAC/C,CAAC;IAED,KAAK;QACH,8BAA8B;QAC9B,KAAK,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACrD,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,MAAM,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;QACrC,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAErB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAClB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACnB,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;CACF"}
@@ -0,0 +1,20 @@
1
+ import type { Node } from "./node.js";
2
+ /**
3
+ * Election handles leader detection and role transitions.
4
+ *
5
+ * On start it attempts to become leader (by binding the port).
6
+ * If the port is taken and a healthy leader is found, it becomes a follower.
7
+ * A periodic ticker monitors the leader and triggers takeover if it dies.
8
+ */
9
+ export declare class Election {
10
+ private port;
11
+ private node;
12
+ private interval;
13
+ private leaderUrl;
14
+ constructor(port: number, node: Node);
15
+ start(): Promise<void>;
16
+ stop(): void;
17
+ private checkAndUpdateRole;
18
+ private determineRole;
19
+ private pingLeader;
20
+ }
@@ -0,0 +1,85 @@
1
+ import { Role } from "./types.js";
2
+ /**
3
+ * Election handles leader detection and role transitions.
4
+ *
5
+ * On start it attempts to become leader (by binding the port).
6
+ * If the port is taken and a healthy leader is found, it becomes a follower.
7
+ * A periodic ticker monitors the leader and triggers takeover if it dies.
8
+ */
9
+ export class Election {
10
+ port;
11
+ node;
12
+ interval = null;
13
+ leaderUrl;
14
+ constructor(port, node) {
15
+ this.port = port;
16
+ this.node = node;
17
+ this.leaderUrl = `http://localhost:${port}`;
18
+ }
19
+ async start() {
20
+ // Determine initial role
21
+ await this.determineRole();
22
+ // Continuous monitoring with random jitter (3-5 s)
23
+ const jitter = 3_000 + Math.random() * 2_000;
24
+ this.interval = setInterval(() => {
25
+ this.checkAndUpdateRole().catch((err) => {
26
+ console.error("Election check error:", err);
27
+ });
28
+ }, jitter);
29
+ }
30
+ stop() {
31
+ if (this.interval) {
32
+ clearInterval(this.interval);
33
+ this.interval = null;
34
+ }
35
+ }
36
+ async checkAndUpdateRole() {
37
+ switch (this.node.role) {
38
+ case Role.Follower: {
39
+ const alive = await this.pingLeader();
40
+ if (!alive) {
41
+ console.error("Leader not responding, attempting takeover...");
42
+ try {
43
+ await this.node.becomeLeader();
44
+ }
45
+ catch (err) {
46
+ console.error("Failed to become leader:", err);
47
+ }
48
+ }
49
+ break;
50
+ }
51
+ case Role.Leader:
52
+ // Nothing to do — we are the leader
53
+ break;
54
+ case Role.Unknown:
55
+ await this.determineRole();
56
+ break;
57
+ }
58
+ }
59
+ async determineRole() {
60
+ // Try to become leader first
61
+ try {
62
+ await this.node.becomeLeader();
63
+ return;
64
+ }
65
+ catch {
66
+ // Port likely in use — check if there's a valid leader
67
+ }
68
+ if (await this.pingLeader()) {
69
+ this.node.becomeFollower();
70
+ }
71
+ // If ping fails too, next tick will retry
72
+ }
73
+ async pingLeader() {
74
+ try {
75
+ const response = await fetch(`${this.leaderUrl}/ping`, {
76
+ signal: AbortSignal.timeout(2_000),
77
+ });
78
+ return response.ok;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ }
85
+ //# sourceMappingURL=election.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"election.js","sourceRoot":"","sources":["../src/election.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC;;;;;;GAMG;AACH,MAAM,OAAO,QAAQ;IAKT;IACA;IALF,QAAQ,GAA0C,IAAI,CAAC;IACvD,SAAS,CAAS;IAE1B,YACU,IAAY,EACZ,IAAU;QADV,SAAI,GAAJ,IAAI,CAAQ;QACZ,SAAI,GAAJ,IAAI,CAAM;QAElB,IAAI,CAAC,SAAS,GAAG,oBAAoB,IAAI,EAAE,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,KAAK;QACT,yBAAyB;QACzB,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;QAE3B,mDAAmD;QACnD,MAAM,MAAM,GAAG,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC;QAC7C,IAAI,CAAC,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE;YAC/B,IAAI,CAAC,kBAAkB,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACtC,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;YAC9C,CAAC,CAAC,CAAC;QACL,CAAC,EAAE,MAAM,CAAC,CAAC;IACb,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,kBAAkB;QAC9B,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACvB,KAAK,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACnB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;gBACtC,IAAI,CAAC,KAAK,EAAE,CAAC;oBACX,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;oBAC/D,IAAI,CAAC;wBACH,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;oBACjC,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAC;oBACjD,CAAC;gBACH,CAAC;gBACD,MAAM;YACR,CAAC;YACD,KAAK,IAAI,CAAC,MAAM;gBACd,oCAAoC;gBACpC,MAAM;YACR,KAAK,IAAI,CAAC,OAAO;gBACf,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;gBAC3B,MAAM;QACV,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,aAAa;QACzB,6BAA6B;QAC7B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YAC/B,OAAO;QACT,CAAC;QAAC,MAAM,CAAC;YACP,uDAAuD;QACzD,CAAC;QAED,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;QAC7B,CAAC;QACD,0CAA0C;IAC5C,CAAC;IAEO,KAAK,CAAC,UAAU;QACtB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,OAAO,EAAE;gBACrD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;aACnC,CAAC,CAAC;YACH,OAAO,QAAQ,CAAC,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,11 @@
1
+ import type { BridgeResponse } from "./types.js";
2
+ /**
3
+ * Follower proxies MCP tool calls to the leader via HTTP /rpc.
4
+ */
5
+ export declare class Follower {
6
+ private leaderUrl;
7
+ constructor(leaderUrl: string);
8
+ send(requestType: string, nodeIds?: string[]): Promise<BridgeResponse>;
9
+ sendWithParams(requestType: string, nodeIds?: string[], params?: Record<string, unknown>): Promise<BridgeResponse>;
10
+ ping(): Promise<boolean>;
11
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Follower proxies MCP tool calls to the leader via HTTP /rpc.
3
+ */
4
+ export class Follower {
5
+ leaderUrl;
6
+ constructor(leaderUrl) {
7
+ this.leaderUrl = leaderUrl;
8
+ }
9
+ send(requestType, nodeIds) {
10
+ return this.sendWithParams(requestType, nodeIds);
11
+ }
12
+ async sendWithParams(requestType, nodeIds, params) {
13
+ const rpcReq = { tool: requestType };
14
+ if (nodeIds && nodeIds.length > 0)
15
+ rpcReq.nodeIds = nodeIds;
16
+ if (params && Object.keys(params).length > 0)
17
+ rpcReq.params = params;
18
+ const response = await fetch(`${this.leaderUrl}/rpc`, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify(rpcReq),
22
+ signal: AbortSignal.timeout(35_000),
23
+ });
24
+ if (!response.ok) {
25
+ throw new Error(`Leader returned status ${response.status}`);
26
+ }
27
+ const rpcResp = (await response.json());
28
+ if (rpcResp.error) {
29
+ throw new Error(rpcResp.error);
30
+ }
31
+ return {
32
+ type: requestType,
33
+ requestId: "",
34
+ data: rpcResp.data,
35
+ };
36
+ }
37
+ async ping() {
38
+ try {
39
+ const response = await fetch(`${this.leaderUrl}/ping`, {
40
+ signal: AbortSignal.timeout(2_000),
41
+ });
42
+ return response.ok;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ }
49
+ //# sourceMappingURL=follower.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"follower.js","sourceRoot":"","sources":["../src/follower.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,OAAO,QAAQ;IACC;IAApB,YAAoB,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;IAEzC,IAAI,CACF,WAAmB,EACnB,OAAkB;QAElB,OAAO,IAAI,CAAC,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,WAAmB,EACnB,OAAkB,EAClB,MAAgC;QAEhC,MAAM,MAAM,GAAe,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QACjD,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;QAC5D,IAAI,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;QAErE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,MAAM,EAAE;YACpD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;YAC5B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;SACpC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAC/D,CAAC;QAED,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAgB,CAAC;QAEvD,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QAED,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,SAAS,EAAE,EAAE;YACb,IAAI,EAAE,OAAO,CAAC,IAAI;SACnB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,OAAO,EAAE;gBACrD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;aACnC,CAAC,CAAC;YACH,OAAO,QAAQ,CAAC,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { Node } from "./node.js";
5
+ import { Election } from "./election.js";
6
+ import { registerTools } from "./tools.js";
7
+ const PORT = 1994;
8
+ async function main() {
9
+ const node = new Node(PORT);
10
+ const election = new Election(PORT, node);
11
+ await election.start();
12
+ // Graceful shutdown
13
+ const shutdown = () => {
14
+ console.error("Shutting down...");
15
+ election.stop();
16
+ node.stop();
17
+ process.exit(0);
18
+ };
19
+ process.on("SIGINT", shutdown);
20
+ process.on("SIGTERM", shutdown);
21
+ // Create MCP server (stdio transport)
22
+ const server = new McpServer({
23
+ name: "figma-bridge",
24
+ version: "0.1.0",
25
+ });
26
+ registerTools(server, node);
27
+ console.error(`Starting MCP server (role: ${node.roleName})`);
28
+ const transport = new StdioServerTransport();
29
+ await server.connect(transport);
30
+ }
31
+ main().catch((err) => {
32
+ console.error("Fatal error:", err);
33
+ process.exit(1);
34
+ });
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,IAAI,GAAG,IAAI,CAAC;AAElB,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC1C,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;IAEvB,oBAAoB;IACpB,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;QAClC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAChB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEhC,sCAAsC;IACtC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAE5B,OAAO,CAAC,KAAK,CAAC,8BAA8B,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;IAE9D,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { Bridge } from "./bridge.js";
2
+ /**
3
+ * Leader owns the WebSocket bridge to Figma and exposes HTTP endpoints for followers.
4
+ * Endpoints:
5
+ * /ws — WebSocket upgrade for the Figma plugin
6
+ * /ping — Health check
7
+ * /rpc — JSON RPC for follower tool calls
8
+ */
9
+ export declare class Leader {
10
+ private port;
11
+ private bridge;
12
+ private server;
13
+ constructor(port: number);
14
+ getBridge(): Bridge;
15
+ start(): Promise<void>;
16
+ private handleRPC;
17
+ stop(): void;
18
+ }
package/dist/leader.js ADDED
@@ -0,0 +1,89 @@
1
+ import http from "node:http";
2
+ import { Bridge } from "./bridge.js";
3
+ /**
4
+ * Leader owns the WebSocket bridge to Figma and exposes HTTP endpoints for followers.
5
+ * Endpoints:
6
+ * /ws — WebSocket upgrade for the Figma plugin
7
+ * /ping — Health check
8
+ * /rpc — JSON RPC for follower tool calls
9
+ */
10
+ export class Leader {
11
+ port;
12
+ bridge;
13
+ server = null;
14
+ constructor(port) {
15
+ this.port = port;
16
+ this.bridge = new Bridge();
17
+ }
18
+ getBridge() {
19
+ return this.bridge;
20
+ }
21
+ start() {
22
+ return new Promise((resolve, reject) => {
23
+ const server = http.createServer((req, res) => {
24
+ if (req.url === "/ping" && req.method === "GET") {
25
+ res.writeHead(200, { "Content-Type": "application/json" });
26
+ res.end(JSON.stringify({ status: "ok", version: "0.1.0" }));
27
+ return;
28
+ }
29
+ if (req.url === "/rpc" && req.method === "POST") {
30
+ this.handleRPC(req, res);
31
+ return;
32
+ }
33
+ res.writeHead(404);
34
+ res.end("Not found");
35
+ });
36
+ server.on("upgrade", (req, socket, head) => {
37
+ if (req.url === "/ws") {
38
+ this.bridge.handleUpgrade(req, socket, head);
39
+ }
40
+ else {
41
+ socket.destroy();
42
+ }
43
+ });
44
+ // Fail fast if port is already in use
45
+ server.once("error", (err) => {
46
+ reject(err.code === "EADDRINUSE"
47
+ ? new Error(`Port ${this.port} already in use`)
48
+ : err);
49
+ });
50
+ server.listen(this.port, () => {
51
+ this.server = server;
52
+ console.error(`Leader listening on :${this.port}`);
53
+ resolve();
54
+ });
55
+ });
56
+ }
57
+ handleRPC(req, res) {
58
+ let body = "";
59
+ req.on("data", (chunk) => {
60
+ body += chunk.toString();
61
+ });
62
+ req.on("end", async () => {
63
+ try {
64
+ const rpcReq = JSON.parse(body);
65
+ const resp = await this.bridge.sendWithParams(rpcReq.tool, rpcReq.nodeIds, rpcReq.params);
66
+ const rpcResp = resp.error
67
+ ? { error: resp.error }
68
+ : { data: resp.data };
69
+ res.writeHead(200, { "Content-Type": "application/json" });
70
+ res.end(JSON.stringify(rpcResp));
71
+ }
72
+ catch (err) {
73
+ const rpcResp = {
74
+ error: err instanceof Error ? err.message : String(err),
75
+ };
76
+ res.writeHead(200, { "Content-Type": "application/json" });
77
+ res.end(JSON.stringify(rpcResp));
78
+ }
79
+ });
80
+ }
81
+ stop() {
82
+ this.bridge.close();
83
+ if (this.server) {
84
+ this.server.close();
85
+ this.server = null;
86
+ }
87
+ }
88
+ }
89
+ //# sourceMappingURL=leader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leader.js","sourceRoot":"","sources":["../src/leader.ts"],"names":[],"mappings":"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"}
package/dist/node.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { Role } from "./types.js";
2
+ import type { BridgeResponse } from "./types.js";
3
+ /**
4
+ * Node is the dynamic handler that switches between leader and follower roles.
5
+ * It routes MCP tool calls to the appropriate backend based on its current role.
6
+ */
7
+ export declare class Node {
8
+ private port;
9
+ private _role;
10
+ private leader;
11
+ private follower;
12
+ constructor(port: number);
13
+ get role(): Role;
14
+ get roleName(): string;
15
+ send(requestType: string, nodeIds?: string[]): Promise<BridgeResponse>;
16
+ sendWithParams(requestType: string, nodeIds?: string[], params?: Record<string, unknown>): Promise<BridgeResponse>;
17
+ becomeLeader(): Promise<void>;
18
+ becomeFollower(): void;
19
+ stop(): void;
20
+ }
package/dist/node.js ADDED
@@ -0,0 +1,68 @@
1
+ import { Leader } from "./leader.js";
2
+ import { Follower } from "./follower.js";
3
+ import { Role } from "./types.js";
4
+ /**
5
+ * Node is the dynamic handler that switches between leader and follower roles.
6
+ * It routes MCP tool calls to the appropriate backend based on its current role.
7
+ */
8
+ export class Node {
9
+ port;
10
+ _role = Role.Unknown;
11
+ leader = null;
12
+ follower;
13
+ constructor(port) {
14
+ this.port = port;
15
+ this.follower = new Follower(`http://localhost:${port}`);
16
+ }
17
+ get role() {
18
+ return this._role;
19
+ }
20
+ get roleName() {
21
+ switch (this._role) {
22
+ case Role.Leader:
23
+ return "LEADER";
24
+ case Role.Follower:
25
+ return "FOLLOWER";
26
+ default:
27
+ return "UNKNOWN";
28
+ }
29
+ }
30
+ send(requestType, nodeIds) {
31
+ return this.sendWithParams(requestType, nodeIds);
32
+ }
33
+ sendWithParams(requestType, nodeIds, params) {
34
+ if (this._role === Role.Leader && this.leader) {
35
+ return this.leader
36
+ .getBridge()
37
+ .sendWithParams(requestType, nodeIds, params);
38
+ }
39
+ return this.follower.sendWithParams(requestType, nodeIds, params);
40
+ }
41
+ async becomeLeader() {
42
+ if (this._role === Role.Leader)
43
+ return;
44
+ const leader = new Leader(this.port);
45
+ await leader.start();
46
+ this.leader = leader;
47
+ this._role = Role.Leader;
48
+ console.error("Became LEADER");
49
+ }
50
+ becomeFollower() {
51
+ if (this._role === Role.Follower)
52
+ return;
53
+ if (this.leader) {
54
+ this.leader.stop();
55
+ this.leader = null;
56
+ }
57
+ this._role = Role.Follower;
58
+ console.error("Became FOLLOWER");
59
+ }
60
+ stop() {
61
+ if (this.leader) {
62
+ this.leader.stop();
63
+ this.leader = null;
64
+ }
65
+ this._role = Role.Unknown;
66
+ }
67
+ }
68
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAGlC;;;GAGG;AACH,MAAM,OAAO,IAAI;IAKK;IAJZ,KAAK,GAAS,IAAI,CAAC,OAAO,CAAC;IAC3B,MAAM,GAAkB,IAAI,CAAC;IAC7B,QAAQ,CAAW;IAE3B,YAAoB,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;QAC9B,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,IAAI,QAAQ;QACV,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,KAAK,IAAI,CAAC,MAAM;gBACd,OAAO,QAAQ,CAAC;YAClB,KAAK,IAAI,CAAC,QAAQ;gBAChB,OAAO,UAAU,CAAC;YACpB;gBACE,OAAO,SAAS,CAAC;QACrB,CAAC;IACH,CAAC;IAED,IAAI,CACF,WAAmB,EACnB,OAAkB;QAElB,OAAO,IAAI,CAAC,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IAED,cAAc,CACZ,WAAmB,EACnB,OAAkB,EAClB,MAAgC;QAEhC,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC9C,OAAO,IAAI,CAAC,MAAM;iBACf,SAAS,EAAE;iBACX,cAAc,CAAC,WAAW,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAClD,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,WAAW,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IACpE,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,MAAM;YAAE,OAAO;QAEvC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QAErB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IACjC,CAAC;IAED,cAAc;QACZ,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,QAAQ;YAAE,OAAO;QAEzC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACnB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACnC,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACnB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { Node } from "./node.js";
3
+ export declare function registerTools(server: McpServer, node: Node): void;
package/dist/tools.js ADDED
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ export function registerTools(server, node) {
3
+ server.tool("get_document", "Get the current Figma page document tree", async () => {
4
+ return renderResponse(() => node.send("get_document"));
5
+ });
6
+ server.tool("get_selection", "Get the currently selected nodes in Figma", async () => {
7
+ return renderResponse(() => node.send("get_selection"));
8
+ });
9
+ server.tool("get_node", "Get a specific Figma node by ID", { nodeId: z.string().describe("The node ID to fetch") }, async ({ nodeId }) => {
10
+ return renderResponse(() => node.send("get_node", [nodeId]));
11
+ });
12
+ server.tool("get_styles", "Get all local styles in the document", async () => {
13
+ return renderResponse(() => node.send("get_styles"));
14
+ });
15
+ server.tool("get_metadata", "Get metadata about the current Figma document including file name, pages, and current page info", async () => {
16
+ return renderResponse(() => node.send("get_metadata"));
17
+ });
18
+ server.tool("get_design_context", "Get the design context for the current selection or page. Returns a summarized tree structure optimized for understanding the current design context.", {
19
+ depth: z
20
+ .number()
21
+ .optional()
22
+ .describe("How many levels deep to traverse the node tree (default 2)"),
23
+ }, async ({ depth }) => {
24
+ const params = {};
25
+ if (depth !== undefined && depth > 0) {
26
+ params.depth = depth;
27
+ }
28
+ return renderResponse(() => node.sendWithParams("get_design_context", undefined, params));
29
+ });
30
+ server.tool("get_variable_defs", "Get all local variable definitions including variable collections, modes, and variable values. Variables are Figma's system for design tokens (colors, numbers, strings, booleans).", async () => {
31
+ return renderResponse(() => node.send("get_variable_defs"));
32
+ });
33
+ server.tool("get_screenshot", "Export a screenshot of the selected nodes or specific nodes by ID. Returns base64-encoded image data.", {
34
+ nodeIds: z
35
+ .array(z.string())
36
+ .optional()
37
+ .describe("Optional list of node IDs to export — if empty, exports the current selection"),
38
+ format: z
39
+ .string()
40
+ .optional()
41
+ .describe("Export format: PNG (default) or SVG or JPG or PDF"),
42
+ scale: z
43
+ .number()
44
+ .optional()
45
+ .describe("Export scale for raster formats (default 2)"),
46
+ }, async ({ nodeIds, format, scale }) => {
47
+ const params = {};
48
+ if (format)
49
+ params.format = format;
50
+ if (scale !== undefined && scale > 0)
51
+ params.scale = scale;
52
+ return renderResponse(() => node.sendWithParams("get_screenshot", nodeIds, params));
53
+ });
54
+ }
55
+ async function renderResponse(fn) {
56
+ try {
57
+ const resp = await fn();
58
+ if (resp.error) {
59
+ return {
60
+ content: [{ type: "text", text: resp.error }],
61
+ isError: true,
62
+ };
63
+ }
64
+ return {
65
+ content: [{ type: "text", text: JSON.stringify(resp.data) }],
66
+ };
67
+ }
68
+ catch (err) {
69
+ return {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: err instanceof Error ? err.message : String(err),
74
+ },
75
+ ],
76
+ isError: true,
77
+ };
78
+ }
79
+ }
80
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export interface BridgeRequest {
2
+ type: string;
3
+ requestId: string;
4
+ nodeIds?: string[];
5
+ params?: Record<string, unknown>;
6
+ }
7
+ export interface BridgeResponse {
8
+ type: string;
9
+ requestId: string;
10
+ data?: unknown;
11
+ error?: string;
12
+ }
13
+ export interface RPCRequest {
14
+ tool: string;
15
+ nodeIds?: string[];
16
+ params?: Record<string, unknown>;
17
+ }
18
+ export interface RPCResponse {
19
+ data?: unknown;
20
+ error?: string;
21
+ }
22
+ export declare enum Role {
23
+ Unknown = 0,
24
+ Leader = 1,
25
+ Follower = 2
26
+ }
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ export var Role;
2
+ (function (Role) {
3
+ Role[Role["Unknown"] = 0] = "Unknown";
4
+ Role[Role["Leader"] = 1] = "Leader";
5
+ Role[Role["Follower"] = 2] = "Follower";
6
+ })(Role || (Role = {}));
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAyBA,MAAM,CAAN,IAAY,IAIX;AAJD,WAAY,IAAI;IACd,qCAAW,CAAA;IACX,mCAAU,CAAA;IACV,uCAAY,CAAA;AACd,CAAC,EAJW,IAAI,KAAJ,IAAI,QAIf"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@gethopp/figma-mcp-bridge",
3
+ "version": "0.0.1",
4
+ "description": "MCP server that bridges Figma plugin data to AI tools without hitting API rate limits",
5
+ "type": "module",
6
+ "bin": {
7
+ "figma-mcp-bridge": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "bun run build"
16
+ },
17
+ "keywords": [
18
+ "figma",
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "ai",
22
+ "design"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/gethopp/figma-mcp-bridge.git",
27
+ "directory": "server"
28
+ },
29
+ "homepage": "https://github.com/gethopp/figma-mcp-bridge#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/gethopp/figma-mcp-bridge/issues"
32
+ },
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.26.0",
36
+ "ws": "^8.18.0",
37
+ "zod": "^3.24.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.0.0",
41
+ "@types/ws": "^8.5.0",
42
+ "typescript": "^5.6.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=20.0.0"
46
+ }
47
+ }