@seed-design/mcp 0.0.6 → 0.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-design/mcp",
3
- "version": "0.0.6",
3
+ "version": "0.0.16",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -12,7 +12,8 @@
12
12
  "sideEffects": false,
13
13
  "files": [
14
14
  "bin",
15
- "src"
15
+ "src",
16
+ "package.json"
16
17
  ],
17
18
  "bin": "./bin/index.mjs",
18
19
  "scripts": {
@@ -22,11 +23,13 @@
22
23
  },
23
24
  "dependencies": {
24
25
  "@modelcontextprotocol/sdk": "^1.7.0",
25
- "@seed-design/figma": "0.0.6",
26
+ "@seed-design/figma": "0.0.15",
27
+ "cac": "^6.7.14",
26
28
  "express": "^4.21.2",
27
29
  "yargs": "^17.7.2"
28
30
  },
29
31
  "devDependencies": {
32
+ "@types/bun": "^1.2.8",
30
33
  "@types/yargs": "^17.0.33",
31
34
  "typescript": "^5.4.5"
32
35
  },
package/src/bin/index.ts CHANGED
@@ -1,34 +1,238 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { startServer } from "../index";
4
- import { ConsoleLogger } from "../logger";
3
+ import { cac } from "cac";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { logger } from "../logger";
7
+ import { createFigmaWebSocketClient } from "../websocket";
8
+ import { registerEditingTools, registerTools } from "../tools";
9
+ import { registerPrompts } from "../prompts";
10
+ import { version } from "../../package.json" assert { type: "json" };
11
+ import type { Server, ServerWebSocket } from "bun";
5
12
 
6
- /**
7
- * Main CLI entry point
8
- */
9
- async function main(): Promise<void> {
10
- try {
11
- // Set environment to indicate CLI mode
12
- process.env["NODE_ENV"] = "cli";
13
+ // Initialize CLI
14
+ const cli = cac("@seed-design/mcp");
13
15
 
14
- // Start server
15
- await startServer();
16
- } catch (error) {
17
- handleStartupError(error);
18
- process.exit(1);
19
- }
16
+ // Store WebSocket clients by channel
17
+ const channels = new Map<string, Set<ServerWebSocket<any>>>();
18
+
19
+ function handleWebSocketConnection(ws: ServerWebSocket<any>) {
20
+ console.log("New client connected");
21
+
22
+ ws.send(
23
+ JSON.stringify({
24
+ type: "system",
25
+ message: "Please join a channel to start chatting",
26
+ }),
27
+ );
28
+
29
+ ws.close = () => {
30
+ console.log("Client disconnected");
31
+ channels.forEach((clients, channelName) => {
32
+ if (clients.has(ws)) {
33
+ clients.delete(ws);
34
+ clients.forEach((client) => {
35
+ if (client.readyState === WebSocket.OPEN) {
36
+ client.send(
37
+ JSON.stringify({
38
+ type: "system",
39
+ message: "A user has left the channel",
40
+ channel: channelName,
41
+ }),
42
+ );
43
+ }
44
+ });
45
+ }
46
+ });
47
+ };
20
48
  }
21
49
 
22
- /**
23
- * Handles and logs startup errors
24
- */
25
- function handleStartupError(error: unknown): void {
26
- if (error instanceof Error) {
27
- ConsoleLogger.error("Failed to start server:", error.message);
28
- } else {
29
- ConsoleLogger.error("Failed to start server with unknown error:", error);
50
+ async function startWebSocketServer(port: number) {
51
+ const server = Bun.serve({
52
+ port,
53
+ // uncomment this to allow connections in windows wsl
54
+ // hostname: "0.0.0.0",
55
+ fetch(req: Request, server: Server) {
56
+ // Handle CORS preflight
57
+ if (req.method === "OPTIONS") {
58
+ return new Response(null, {
59
+ headers: {
60
+ "Access-Control-Allow-Origin": "*",
61
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
62
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
63
+ },
64
+ });
65
+ }
66
+
67
+ // Handle WebSocket upgrade
68
+ const success = server.upgrade(req, {
69
+ headers: {
70
+ "Access-Control-Allow-Origin": "*",
71
+ },
72
+ });
73
+
74
+ if (success) return;
75
+
76
+ // Return response for non-WebSocket requests
77
+ return new Response("WebSocket server running", {
78
+ headers: {
79
+ "Access-Control-Allow-Origin": "*",
80
+ },
81
+ });
82
+ },
83
+ websocket: {
84
+ open: handleWebSocketConnection,
85
+ message(ws: ServerWebSocket<any>, message: string | Buffer) {
86
+ try {
87
+ console.log("Received message from client:", message);
88
+ const data = JSON.parse(message as string);
89
+
90
+ if (data.type === "join") {
91
+ const channelName = data.channel;
92
+ if (!channelName || typeof channelName !== "string") {
93
+ ws.send(JSON.stringify({ type: "error", message: "Channel name is required" }));
94
+ return;
95
+ }
96
+
97
+ // Create channel if it doesn't exist
98
+ if (!channels.has(channelName)) {
99
+ channels.set(channelName, new Set());
100
+ }
101
+
102
+ // Add client to channel
103
+ const channelClients = channels.get(channelName)!;
104
+ channelClients.add(ws);
105
+
106
+ // Notify client they joined successfully
107
+ ws.send(
108
+ JSON.stringify({
109
+ type: "system",
110
+ message: `Joined channel: ${channelName}`,
111
+ channel: channelName,
112
+ }),
113
+ );
114
+
115
+ console.log("Sending message to client:", data.id);
116
+
117
+ ws.send(
118
+ JSON.stringify({
119
+ type: "system",
120
+ message: {
121
+ id: data.id,
122
+ result: "Connected to channel: " + channelName,
123
+ },
124
+ channel: channelName,
125
+ }),
126
+ );
127
+
128
+ // Notify other clients in channel
129
+ channelClients.forEach((client) => {
130
+ if (client !== ws && client.readyState === WebSocket.OPEN) {
131
+ client.send(
132
+ JSON.stringify({
133
+ type: "system",
134
+ message: "A new user has joined the channel",
135
+ channel: channelName,
136
+ }),
137
+ );
138
+ }
139
+ });
140
+ return;
141
+ }
142
+
143
+ // Handle regular messages
144
+ if (data.type === "message") {
145
+ const channelName = data.channel;
146
+ if (!channelName || typeof channelName !== "string") {
147
+ ws.send(JSON.stringify({ type: "error", message: "Channel name is required" }));
148
+ return;
149
+ }
150
+
151
+ const channelClients = channels.get(channelName);
152
+ if (!channelClients || !channelClients.has(ws)) {
153
+ ws.send(
154
+ JSON.stringify({ type: "error", message: "You must join the channel first" }),
155
+ );
156
+ return;
157
+ }
158
+
159
+ // Broadcast to all clients in the channel
160
+ channelClients.forEach((client) => {
161
+ if (client.readyState === WebSocket.OPEN) {
162
+ console.log("Broadcasting message to client:", data.message);
163
+ client.send(
164
+ JSON.stringify({
165
+ type: "broadcast",
166
+ message: data.message,
167
+ sender: client === ws ? "You" : "User",
168
+ channel: channelName,
169
+ }),
170
+ );
171
+ }
172
+ });
173
+ }
174
+ } catch (err) {
175
+ console.error("Error handling message:", err);
176
+ }
177
+ },
178
+ close(ws: ServerWebSocket<any>) {
179
+ // Remove client from their channel
180
+ channels.forEach((clients) => {
181
+ clients.delete(ws);
182
+ });
183
+ },
184
+ },
185
+ });
186
+
187
+ console.log(`WebSocket server running on port ${server.port}`);
188
+ return server;
189
+ }
190
+
191
+ async function startMcpServer(serverUrl: string, experimental: boolean) {
192
+ const figmaClient = createFigmaWebSocketClient(serverUrl);
193
+ const server = new McpServer({
194
+ name: "SEED Design MCP",
195
+ version,
196
+ });
197
+
198
+ registerTools(server, figmaClient);
199
+ if (experimental) {
200
+ registerEditingTools(server, figmaClient);
30
201
  }
202
+ registerPrompts(server);
203
+
204
+ try {
205
+ figmaClient.connectToFigma();
206
+ } catch (error) {
207
+ logger.warn(
208
+ `Could not connect to Figma initially: ${error instanceof Error ? error.message : String(error)}`,
209
+ );
210
+ logger.warn("Will try to connect when the first command is sent");
211
+ }
212
+
213
+ const transport = new StdioServerTransport();
214
+ await server.connect(transport);
215
+ logger.info("FigmaMCP server running on stdio");
31
216
  }
32
217
 
33
- // Run the application
34
- main().catch(handleStartupError);
218
+ // Define CLI commands
219
+ cli
220
+ .command("", "Start the MCP server")
221
+ .option("--server <server>", "Server URL", { default: "localhost" })
222
+ .option("--experimental", "Enable experimental features", { default: false })
223
+ .action(async (options) => {
224
+ await startMcpServer(options.server, options.experimental);
225
+ });
226
+
227
+ cli
228
+ .command("socket", "Start the WebSocket server")
229
+ .option("--port <port>", "Port number", { default: 3055 })
230
+ .action(async (options) => {
231
+ await startWebSocketServer(options.port);
232
+ });
233
+
234
+ cli.help();
235
+ cli.version(version);
236
+
237
+ // Parse CLI args
238
+ cli.parse();
package/src/logger.ts CHANGED
@@ -1,43 +1,41 @@
1
1
  /**
2
- * Centralized logger module for the Figma MCP Server
2
+ * Custom logging module that writes to stderr instead of stdout
3
+ * to avoid being captured in MCP protocol communication
3
4
  */
4
- export interface Logger {
5
- log(...args: any[]): void;
6
- error(...args: any[]): void;
7
- }
8
5
 
9
- /**
10
- * Default logger implementation that does nothing
11
- */
12
- export const NoOpLogger: Logger = {
13
- log: () => {},
14
- error: () => {},
15
- };
6
+ export const logger = {
7
+ /**
8
+ * Log an informational message
9
+ */
10
+ info: (message: string) => process.stderr.write(`[INFO] ${message}\n`),
16
11
 
17
- /**
18
- * Logger that writes to the console
19
- */
20
- export const ConsoleLogger: Logger = {
21
- log: console.log,
22
- error: console.error,
12
+ /**
13
+ * Log a debug message
14
+ */
15
+ debug: (message: string) => process.stderr.write(`[DEBUG] ${message}\n`),
16
+
17
+ /**
18
+ * Log a warning message
19
+ */
20
+ warn: (message: string) => process.stderr.write(`[WARN] ${message}\n`),
21
+
22
+ /**
23
+ * Log an error message
24
+ */
25
+ error: (message: string) => process.stderr.write(`[ERROR] ${message}\n`),
26
+
27
+ /**
28
+ * Log a general message
29
+ */
30
+ log: (message: string) => process.stderr.write(`[LOG] ${message}\n`),
23
31
  };
24
32
 
25
33
  /**
26
- * Creates a logger that sends messages to an MCP server
34
+ * Format an error for logging
27
35
  */
28
- export function createMcpLogger(sendLoggingMessage: (message: any) => void): Logger {
29
- return {
30
- log: (...args: any[]) => {
31
- sendLoggingMessage({
32
- level: "info",
33
- data: args,
34
- });
35
- },
36
- error: (...args: any[]) => {
37
- sendLoggingMessage({
38
- level: "error",
39
- data: args,
40
- });
41
- },
42
- };
36
+ export function formatError(error: unknown): string {
37
+ if (error instanceof Error) {
38
+ return error.message;
39
+ }
40
+ return String(error);
43
41
  }
package/src/prompts.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ export function registerPrompts(server: McpServer): void {
4
+ server.prompt(
5
+ "react_implementation_strategy",
6
+ "Best practices for implementing React components",
7
+ (_extra) => {
8
+ return {
9
+ messages: [
10
+ {
11
+ role: "assistant",
12
+ content: {
13
+ type: "text",
14
+ text: `When implementing React components, follow these best practices:
15
+
16
+ 1. Start with selection:
17
+ - First use get_selection() to understand the current selection
18
+ - If no selection ask user to select single node
19
+
20
+ 2. Get React code of the selected nodes:
21
+ - Use get_node_react_code() to get the React code of the selected node
22
+ - If no selection ask user to select single node
23
+ `,
24
+ },
25
+ },
26
+ ],
27
+ description: "Best practices for reading Figma designs",
28
+ };
29
+ },
30
+ );
31
+
32
+ server.prompt("read_design_strategy", "Best practices for reading Figma designs", (_extra) => {
33
+ return {
34
+ messages: [
35
+ {
36
+ role: "assistant",
37
+ content: {
38
+ type: "text",
39
+ text: `When reading Figma designs, follow these best practices:
40
+
41
+ 1. Start with selection:
42
+ - First use get_selection() to understand the current selection
43
+ - If no selection ask user to select single or multiple nodes
44
+
45
+ 2. Get node infos of the selected nodes:
46
+ - Use get_nodes_info() to get the information of the selected nodes
47
+ - If no selection ask user to select single or multiple nodes
48
+ `,
49
+ },
50
+ },
51
+ ],
52
+ description: "Best practices for reading Figma designs",
53
+ };
54
+ });
55
+ }
@@ -0,0 +1,90 @@
1
+ import { formatError } from "./logger";
2
+
3
+ /**
4
+ * Helper type for a tool response content item
5
+ */
6
+ export type ContentItem =
7
+ | { type: "text"; text: string }
8
+ | { type: "image"; data: string; mimeType: string };
9
+
10
+ /**
11
+ * Helper type for a tool response
12
+ */
13
+ export type ToolResponse = {
14
+ content: ContentItem[];
15
+ };
16
+
17
+ /**
18
+ * Format an object response
19
+ */
20
+ export function formatObjectResponse(result: unknown): ToolResponse {
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: JSON.stringify(result),
26
+ },
27
+ ],
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Format a text response
33
+ */
34
+ export function formatTextResponse(text: string): ToolResponse {
35
+ return {
36
+ content: [
37
+ {
38
+ type: "text",
39
+ text,
40
+ },
41
+ ],
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Format an image response
47
+ */
48
+ export function formatImageResponse(imageData: string, mimeType = "image/png"): ToolResponse {
49
+ return {
50
+ content: [
51
+ {
52
+ type: "image",
53
+ data: imageData,
54
+ mimeType,
55
+ },
56
+ ],
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Format an error response
62
+ */
63
+ export function formatErrorResponse(toolName: string, error: unknown): ToolResponse {
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: `Error in ${toolName}: ${formatError(error)}`,
69
+ },
70
+ ],
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Format a progress response with initial message
76
+ */
77
+ export function formatProgressResponse(initialMessage: string, result: unknown): ToolResponse {
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: initialMessage,
83
+ },
84
+ {
85
+ type: "text",
86
+ text: typeof result === "string" ? result : JSON.stringify(result),
87
+ },
88
+ ],
89
+ };
90
+ }