@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.
package/bin/socket.mjs ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ // Store clients by channel
3
+ const channels = new Map();
4
+ function handleConnection(ws) {
5
+ // Don't add to clients immediately - wait for channel join
6
+ console.log("New client connected");
7
+ // Send welcome message to the new client
8
+ ws.send(JSON.stringify({
9
+ type: "system",
10
+ message: "Please join a channel to start chatting"
11
+ }));
12
+ ws.close = ()=>{
13
+ console.log("Client disconnected");
14
+ // Remove client from their channel
15
+ channels.forEach((clients, channelName)=>{
16
+ if (clients.has(ws)) {
17
+ clients.delete(ws);
18
+ // Notify other clients in same channel
19
+ clients.forEach((client)=>{
20
+ if (client.readyState === WebSocket.OPEN) {
21
+ client.send(JSON.stringify({
22
+ type: "system",
23
+ message: "A user has left the channel",
24
+ channel: channelName
25
+ }));
26
+ }
27
+ });
28
+ }
29
+ });
30
+ };
31
+ }
32
+ const server = Bun.serve({
33
+ port: 3055,
34
+ // uncomment this to allow connections in windows wsl
35
+ // hostname: "0.0.0.0",
36
+ fetch (req, server) {
37
+ // Handle CORS preflight
38
+ if (req.method === "OPTIONS") {
39
+ return new Response(null, {
40
+ headers: {
41
+ "Access-Control-Allow-Origin": "*",
42
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
43
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
44
+ }
45
+ });
46
+ }
47
+ // Handle WebSocket upgrade
48
+ const success = server.upgrade(req, {
49
+ headers: {
50
+ "Access-Control-Allow-Origin": "*"
51
+ }
52
+ });
53
+ if (success) {
54
+ return; // Upgraded to WebSocket
55
+ }
56
+ // Return response for non-WebSocket requests
57
+ return new Response("WebSocket server running", {
58
+ headers: {
59
+ "Access-Control-Allow-Origin": "*"
60
+ }
61
+ });
62
+ },
63
+ websocket: {
64
+ open: handleConnection,
65
+ message (ws, message) {
66
+ try {
67
+ console.log("Received message from client:", message);
68
+ const data = JSON.parse(message);
69
+ if (data.type === "join") {
70
+ const channelName = data.channel;
71
+ if (!channelName || typeof channelName !== "string") {
72
+ ws.send(JSON.stringify({
73
+ type: "error",
74
+ message: "Channel name is required"
75
+ }));
76
+ return;
77
+ }
78
+ // Create channel if it doesn't exist
79
+ if (!channels.has(channelName)) {
80
+ channels.set(channelName, new Set());
81
+ }
82
+ // Add client to channel
83
+ const channelClients = channels.get(channelName);
84
+ channelClients.add(ws);
85
+ // Notify client they joined successfully
86
+ ws.send(JSON.stringify({
87
+ type: "system",
88
+ message: `Joined channel: ${channelName}`,
89
+ channel: channelName
90
+ }));
91
+ console.log("Sending message to client:", data.id);
92
+ ws.send(JSON.stringify({
93
+ type: "system",
94
+ message: {
95
+ id: data.id,
96
+ result: "Connected to channel: " + channelName
97
+ },
98
+ channel: channelName
99
+ }));
100
+ // Notify other clients in channel
101
+ channelClients.forEach((client)=>{
102
+ if (client !== ws && client.readyState === WebSocket.OPEN) {
103
+ client.send(JSON.stringify({
104
+ type: "system",
105
+ message: "A new user has joined the channel",
106
+ channel: channelName
107
+ }));
108
+ }
109
+ });
110
+ return;
111
+ }
112
+ // Handle regular messages
113
+ if (data.type === "message") {
114
+ const channelName = data.channel;
115
+ if (!channelName || typeof channelName !== "string") {
116
+ ws.send(JSON.stringify({
117
+ type: "error",
118
+ message: "Channel name is required"
119
+ }));
120
+ return;
121
+ }
122
+ const channelClients = channels.get(channelName);
123
+ if (!channelClients || !channelClients.has(ws)) {
124
+ ws.send(JSON.stringify({
125
+ type: "error",
126
+ message: "You must join the channel first"
127
+ }));
128
+ return;
129
+ }
130
+ // Broadcast to all clients in the channel
131
+ channelClients.forEach((client)=>{
132
+ if (client.readyState === WebSocket.OPEN) {
133
+ console.log("Broadcasting message to client:", data.message);
134
+ client.send(JSON.stringify({
135
+ type: "broadcast",
136
+ message: data.message,
137
+ sender: client === ws ? "You" : "User",
138
+ channel: channelName
139
+ }));
140
+ }
141
+ });
142
+ }
143
+ } catch (err) {
144
+ console.error("Error handling message:", err);
145
+ }
146
+ },
147
+ close (ws) {
148
+ // Remove client from their channel
149
+ channels.forEach((clients)=>{
150
+ clients.delete(ws);
151
+ });
152
+ }
153
+ }
154
+ });
155
+ console.log(`WebSocket server running on port ${server.port}`);
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.15",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -14,19 +14,24 @@
14
14
  "bin",
15
15
  "src"
16
16
  ],
17
- "bin": "./bin/index.mjs",
17
+ "bin": {
18
+ "main": "./bin/main.mjs",
19
+ "socket": "./bin/socket.mjs"
20
+ },
18
21
  "scripts": {
19
22
  "clean": "rm -rf lib",
23
+ "dev": "bun run --watch ./src/bin/socket.ts",
20
24
  "build": "bunchee",
21
25
  "lint:publish": "bun publint"
22
26
  },
23
27
  "dependencies": {
24
28
  "@modelcontextprotocol/sdk": "^1.7.0",
25
- "@seed-design/figma": "0.0.6",
29
+ "@seed-design/figma": "0.0.15",
26
30
  "express": "^4.21.2",
27
31
  "yargs": "^17.7.2"
28
32
  },
29
33
  "devDependencies": {
34
+ "@types/bun": "^1.2.8",
30
35
  "@types/yargs": "^17.0.33",
31
36
  "typescript": "^5.4.5"
32
37
  },
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { logger } from "../logger";
6
+ import { createFigmaWebSocketClient } from "../websocket";
7
+ import { registerTools } from "../tools";
8
+ import { registerPrompts } from "../prompts";
9
+
10
+ // Add command line argument parsing
11
+ const args = process.argv.slice(2);
12
+ const serverArg = args.find((arg) => arg.startsWith("--server="));
13
+ const serverUrl = serverArg ? serverArg.split("=")[1] : "localhost";
14
+
15
+ // Create Figma WebSocket client
16
+ const figmaClient = createFigmaWebSocketClient(serverUrl);
17
+
18
+ // Create MCP server
19
+ const server = new McpServer({
20
+ name: "SEED Design MCP",
21
+ version: "1.0.0",
22
+ });
23
+
24
+ // Register tools and prompts
25
+ registerTools(server, figmaClient);
26
+ registerPrompts(server);
27
+
28
+ // Start the server
29
+ async function main() {
30
+ try {
31
+ // Try to connect to Figma socket server
32
+ figmaClient.connectToFigma();
33
+ } catch (error) {
34
+ logger.warn(
35
+ `Could not connect to Figma initially: ${error instanceof Error ? error.message : String(error)}`,
36
+ );
37
+ logger.warn("Will try to connect when the first command is sent");
38
+ }
39
+
40
+ // Start the MCP server with stdio transport
41
+ const transport = new StdioServerTransport();
42
+ await server.connect(transport);
43
+ logger.info("FigmaMCP server running on stdio");
44
+ }
45
+
46
+ // Run the server
47
+ main().catch((error) => {
48
+ logger.error(
49
+ `Error starting FigmaMCP server: ${error instanceof Error ? error.message : String(error)}`,
50
+ );
51
+ process.exit(1);
52
+ });
@@ -0,0 +1,194 @@
1
+ import type { Server, ServerWebSocket } from "bun";
2
+
3
+ // Store clients by channel
4
+ const channels = new Map<string, Set<ServerWebSocket<any>>>();
5
+
6
+ function handleConnection(ws: ServerWebSocket<any>) {
7
+ // Don't add to clients immediately - wait for channel join
8
+ console.log("New client connected");
9
+
10
+ // Send welcome message to the new client
11
+ ws.send(
12
+ JSON.stringify({
13
+ type: "system",
14
+ message: "Please join a channel to start chatting",
15
+ }),
16
+ );
17
+
18
+ ws.close = () => {
19
+ console.log("Client disconnected");
20
+
21
+ // Remove client from their channel
22
+ channels.forEach((clients, channelName) => {
23
+ if (clients.has(ws)) {
24
+ clients.delete(ws);
25
+
26
+ // Notify other clients in same channel
27
+ clients.forEach((client) => {
28
+ if (client.readyState === WebSocket.OPEN) {
29
+ client.send(
30
+ JSON.stringify({
31
+ type: "system",
32
+ message: "A user has left the channel",
33
+ channel: channelName,
34
+ }),
35
+ );
36
+ }
37
+ });
38
+ }
39
+ });
40
+ };
41
+ }
42
+
43
+ const server = Bun.serve({
44
+ port: 3055,
45
+ // uncomment this to allow connections in windows wsl
46
+ // hostname: "0.0.0.0",
47
+ fetch(req: Request, server: Server) {
48
+ // Handle CORS preflight
49
+ if (req.method === "OPTIONS") {
50
+ return new Response(null, {
51
+ headers: {
52
+ "Access-Control-Allow-Origin": "*",
53
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
54
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
55
+ },
56
+ });
57
+ }
58
+
59
+ // Handle WebSocket upgrade
60
+ const success = server.upgrade(req, {
61
+ headers: {
62
+ "Access-Control-Allow-Origin": "*",
63
+ },
64
+ });
65
+
66
+ if (success) {
67
+ return; // Upgraded to WebSocket
68
+ }
69
+
70
+ // Return response for non-WebSocket requests
71
+ return new Response("WebSocket server running", {
72
+ headers: {
73
+ "Access-Control-Allow-Origin": "*",
74
+ },
75
+ });
76
+ },
77
+ websocket: {
78
+ open: handleConnection,
79
+ message(ws: ServerWebSocket<any>, message: string | Buffer) {
80
+ try {
81
+ console.log("Received message from client:", message);
82
+ const data = JSON.parse(message as string);
83
+
84
+ if (data.type === "join") {
85
+ const channelName = data.channel;
86
+ if (!channelName || typeof channelName !== "string") {
87
+ ws.send(
88
+ JSON.stringify({
89
+ type: "error",
90
+ message: "Channel name is required",
91
+ }),
92
+ );
93
+ return;
94
+ }
95
+
96
+ // Create channel if it doesn't exist
97
+ if (!channels.has(channelName)) {
98
+ channels.set(channelName, new Set());
99
+ }
100
+
101
+ // Add client to channel
102
+ const channelClients = channels.get(channelName)!;
103
+ channelClients.add(ws);
104
+
105
+ // Notify client they joined successfully
106
+ ws.send(
107
+ JSON.stringify({
108
+ type: "system",
109
+ message: `Joined channel: ${channelName}`,
110
+ channel: channelName,
111
+ }),
112
+ );
113
+
114
+ console.log("Sending message to client:", data.id);
115
+
116
+ ws.send(
117
+ JSON.stringify({
118
+ type: "system",
119
+ message: {
120
+ id: data.id,
121
+ result: "Connected to channel: " + channelName,
122
+ },
123
+ channel: channelName,
124
+ }),
125
+ );
126
+
127
+ // Notify other clients in channel
128
+ channelClients.forEach((client) => {
129
+ if (client !== ws && client.readyState === WebSocket.OPEN) {
130
+ client.send(
131
+ JSON.stringify({
132
+ type: "system",
133
+ message: "A new user has joined the channel",
134
+ channel: channelName,
135
+ }),
136
+ );
137
+ }
138
+ });
139
+ return;
140
+ }
141
+
142
+ // Handle regular messages
143
+ if (data.type === "message") {
144
+ const channelName = data.channel;
145
+ if (!channelName || typeof channelName !== "string") {
146
+ ws.send(
147
+ JSON.stringify({
148
+ type: "error",
149
+ message: "Channel name is required",
150
+ }),
151
+ );
152
+ return;
153
+ }
154
+
155
+ const channelClients = channels.get(channelName);
156
+ if (!channelClients || !channelClients.has(ws)) {
157
+ ws.send(
158
+ JSON.stringify({
159
+ type: "error",
160
+ message: "You must join the channel first",
161
+ }),
162
+ );
163
+ return;
164
+ }
165
+
166
+ // Broadcast to all clients in the channel
167
+ channelClients.forEach((client) => {
168
+ if (client.readyState === WebSocket.OPEN) {
169
+ console.log("Broadcasting message to client:", data.message);
170
+ client.send(
171
+ JSON.stringify({
172
+ type: "broadcast",
173
+ message: data.message,
174
+ sender: client === ws ? "You" : "User",
175
+ channel: channelName,
176
+ }),
177
+ );
178
+ }
179
+ });
180
+ }
181
+ } catch (err) {
182
+ console.error("Error handling message:", err);
183
+ }
184
+ },
185
+ close(ws: ServerWebSocket<any>) {
186
+ // Remove client from their channel
187
+ channels.forEach((clients) => {
188
+ clients.delete(ws);
189
+ });
190
+ },
191
+ },
192
+ });
193
+
194
+ console.log(`WebSocket server running on port ${server.port}`);
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,27 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ export function registerPrompts(server: McpServer): void {
4
+ server.prompt("read_design_strategy", "Best practices for reading Figma designs", (_extra) => {
5
+ return {
6
+ messages: [
7
+ {
8
+ role: "assistant",
9
+ content: {
10
+ type: "text",
11
+ text: `When reading Figma designs, follow these best practices:
12
+
13
+ 1. Start with selection:
14
+ - First use get_selection() to understand the current selection
15
+ - If no selection ask user to select single or multiple nodes
16
+
17
+ 2. Get node infos of the selected nodes:
18
+ - Use get_nodes_info() to get the information of the selected nodes
19
+ - If no selection ask user to select single or multiple nodes
20
+ `,
21
+ },
22
+ },
23
+ ],
24
+ description: "Best practices for reading Figma designs",
25
+ };
26
+ });
27
+ }
@@ -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
+ }