@seed-design/mcp 0.0.6 → 0.0.15

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,248 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import WebSocket from "ws";
3
+ import { logger } from "./logger";
4
+ import type { CommandProgressUpdate, FigmaCommand, FigmaResponse } from "./types";
5
+
6
+ export interface FigmaWebSocketClient {
7
+ connectToFigma: (port?: number) => void;
8
+ joinChannel: (channelName: string) => Promise<void>;
9
+ sendCommandToFigma: (
10
+ command: FigmaCommand,
11
+ params?: unknown,
12
+ timeoutMs?: number,
13
+ ) => Promise<unknown>;
14
+ }
15
+
16
+ // Define a more specific type with an index signature to allow any property access
17
+ interface ProgressMessage {
18
+ message: FigmaResponse | any;
19
+ type?: string;
20
+ id?: string;
21
+ [key: string]: any; // Allow any other properties
22
+ }
23
+
24
+ export function createFigmaWebSocketClient(serverUrl: string) {
25
+ const WS_URL = serverUrl === "localhost" ? `ws://${serverUrl}` : `wss://${serverUrl}`;
26
+
27
+ // Track which channel each client is in
28
+ let currentChannel: string | null = null;
29
+
30
+ // WebSocket connection and request tracking
31
+ let ws: WebSocket | null = null;
32
+ const pendingRequests = new Map<
33
+ string,
34
+ {
35
+ resolve: (value: unknown) => void;
36
+ reject: (reason: unknown) => void;
37
+ timeout: ReturnType<typeof setTimeout>;
38
+ lastActivity: number; // Add timestamp for last activity
39
+ }
40
+ >();
41
+
42
+ // Update the connectToFigma function
43
+ function connectToFigma(port = 3055) {
44
+ // If already connected, do nothing
45
+ if (ws && ws.readyState === WebSocket.OPEN) {
46
+ logger.info("Already connected to Figma");
47
+ return;
48
+ }
49
+
50
+ const wsUrl = serverUrl === "localhost" ? `${WS_URL}:${port}` : WS_URL;
51
+ logger.info(`Connecting to Figma socket server at ${wsUrl}...`);
52
+ ws = new WebSocket(wsUrl);
53
+
54
+ ws.on("open", () => {
55
+ logger.info("Connected to Figma socket server");
56
+ // Reset channel on new connection
57
+ currentChannel = null;
58
+ });
59
+
60
+ ws.on("message", (data: any) => {
61
+ try {
62
+ const json = JSON.parse(data) as ProgressMessage;
63
+
64
+ // Handle progress updates
65
+ if (json.type === "progress_update") {
66
+ const progressData = json.message.data as CommandProgressUpdate;
67
+ const requestId = json.id || "";
68
+
69
+ if (requestId && pendingRequests.has(requestId)) {
70
+ const request = pendingRequests.get(requestId)!;
71
+
72
+ // Update last activity timestamp
73
+ request.lastActivity = Date.now();
74
+
75
+ // Reset the timeout to prevent timeouts during long-running operations
76
+ clearTimeout(request.timeout);
77
+
78
+ // Create a new timeout
79
+ request.timeout = setTimeout(() => {
80
+ if (pendingRequests.has(requestId)) {
81
+ logger.error(`Request ${requestId} timed out after extended period of inactivity`);
82
+ pendingRequests.delete(requestId);
83
+ request.reject(new Error("Request to Figma timed out"));
84
+ }
85
+ }, 60000); // 60 second timeout for inactivity
86
+
87
+ // Log progress
88
+ logger.info(
89
+ `Progress update for ${progressData.commandType}: ${progressData.progress}% - ${progressData.message}`,
90
+ );
91
+
92
+ // For completed updates, we could resolve the request early if desired
93
+ if (progressData.status === "completed" && progressData.progress === 100) {
94
+ // Optionally resolve early with partial data
95
+ // request.resolve(progressData.payload);
96
+ // pendingRequests.delete(requestId);
97
+
98
+ // Instead, just log the completion, wait for final result from Figma
99
+ logger.info(
100
+ `Operation ${progressData.commandType} completed, waiting for final result`,
101
+ );
102
+ }
103
+ }
104
+ return;
105
+ }
106
+
107
+ // Handle regular responses
108
+ const myResponse = json.message;
109
+ logger.debug(`Received message: ${JSON.stringify(myResponse)}`);
110
+ logger.log("myResponse" + JSON.stringify(myResponse));
111
+
112
+ // Handle response to a request
113
+ if (
114
+ myResponse.id &&
115
+ pendingRequests.has(myResponse.id) &&
116
+ (myResponse.result || myResponse.error)
117
+ ) {
118
+ const request = pendingRequests.get(myResponse.id)!;
119
+ clearTimeout(request.timeout);
120
+
121
+ if (myResponse.error) {
122
+ logger.error(`Error from Figma: ${myResponse.error}`);
123
+ request.reject(new Error(myResponse.error));
124
+ } else {
125
+ if (myResponse.result) {
126
+ request.resolve(myResponse.result);
127
+ }
128
+ }
129
+
130
+ pendingRequests.delete(myResponse.id);
131
+ } else {
132
+ // Handle broadcast messages or events not associated with a request ID
133
+ logger.info(`Received broadcast message: ${JSON.stringify(myResponse)}`);
134
+ }
135
+ } catch (error) {
136
+ logger.error(
137
+ `Error parsing message: ${error instanceof Error ? error.message : String(error)}`,
138
+ );
139
+ }
140
+ });
141
+
142
+ ws.on("error", (error) => {
143
+ logger.error(`Socket error: ${error}`);
144
+ });
145
+
146
+ ws.on("close", () => {
147
+ logger.info("Disconnected from Figma socket server");
148
+ ws = null;
149
+
150
+ // Reject all pending requests
151
+ for (const [id, request] of pendingRequests.entries()) {
152
+ clearTimeout(request.timeout);
153
+ request.reject(new Error("Connection closed"));
154
+ pendingRequests.delete(id);
155
+ }
156
+
157
+ // Attempt to reconnect
158
+ logger.info("Attempting to reconnect in 2 seconds...");
159
+ setTimeout(() => connectToFigma(port), 2000);
160
+ });
161
+ }
162
+
163
+ // Function to join a channel
164
+ async function joinChannel(channelName: string): Promise<void> {
165
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
166
+ throw new Error("Not connected to Figma");
167
+ }
168
+
169
+ try {
170
+ await sendCommandToFigma("join", { channel: channelName });
171
+ currentChannel = channelName;
172
+ logger.info(`Joined channel: ${channelName}`);
173
+ } catch (error) {
174
+ logger.error(
175
+ `Failed to join channel: ${error instanceof Error ? error.message : String(error)}`,
176
+ );
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ // Function to send commands to Figma
182
+ function sendCommandToFigma(
183
+ command: FigmaCommand,
184
+ params: unknown = {},
185
+ timeoutMs = 30000,
186
+ ): Promise<unknown> {
187
+ return new Promise((resolve, reject) => {
188
+ // If not connected, try to connect first
189
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
190
+ connectToFigma();
191
+ reject(new Error("Not connected to Figma. Attempting to connect..."));
192
+ return;
193
+ }
194
+
195
+ // Check if we need a channel for this command
196
+ const requiresChannel = command !== "join";
197
+ if (requiresChannel && !currentChannel) {
198
+ reject(new Error("Must join a channel before sending commands"));
199
+ return;
200
+ }
201
+
202
+ const id = uuidv4();
203
+ const request = {
204
+ id,
205
+ type: command === "join" ? "join" : "message",
206
+ ...(command === "join"
207
+ ? { channel: (params as any).channel }
208
+ : { channel: currentChannel }),
209
+ message: {
210
+ id,
211
+ command,
212
+ params: {
213
+ ...(params as any),
214
+ commandId: id, // Include the command ID in params
215
+ },
216
+ },
217
+ };
218
+
219
+ // Set timeout for request
220
+ const timeout = setTimeout(() => {
221
+ if (pendingRequests.has(id)) {
222
+ pendingRequests.delete(id);
223
+ logger.error(`Request ${id} to Figma timed out after ${timeoutMs / 1000} seconds`);
224
+ reject(new Error("Request to Figma timed out"));
225
+ }
226
+ }, timeoutMs);
227
+
228
+ // Store the promise callbacks to resolve/reject later
229
+ pendingRequests.set(id, {
230
+ resolve,
231
+ reject,
232
+ timeout,
233
+ lastActivity: Date.now(),
234
+ });
235
+
236
+ // Send the request
237
+ logger.info(`Sending command to Figma: ${command}`);
238
+ logger.debug(`Request details: ${JSON.stringify(request)}`);
239
+ ws.send(JSON.stringify(request));
240
+ });
241
+ }
242
+
243
+ return {
244
+ connectToFigma,
245
+ joinChannel,
246
+ sendCommandToFigma,
247
+ };
248
+ }