@seed-design/mcp 0.0.0-alpha-20260324091316
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 +5 -0
- package/bin/index.mjs +1295 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +581 -0
- package/package.json +50 -0
- package/src/bin/index.ts +197 -0
- package/src/bin/websocket-server.ts +193 -0
- package/src/config.ts +47 -0
- package/src/figma-rest-client.ts +55 -0
- package/src/index.ts +4 -0
- package/src/logger.ts +41 -0
- package/src/prompts.ts +55 -0
- package/src/responses.ts +77 -0
- package/src/tools-helpers.ts +113 -0
- package/src/tools.ts +763 -0
- package/src/types.ts +67 -0
- package/src/websocket.ts +248 -0
package/src/bin/index.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
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 { version } from "../../package.json" with { type: "json" };
|
|
7
|
+
import { logger } from "../logger";
|
|
8
|
+
import { loadConfig, type McpConfig } from "../config";
|
|
9
|
+
import { createFigmaRestClient, type FigmaRestClient } from "../figma-rest-client";
|
|
10
|
+
import { createFigmaWebSocketClient, type FigmaWebSocketClient } from "../websocket";
|
|
11
|
+
import { registerEditingTools, registerTools } from "../tools";
|
|
12
|
+
import type { ToolMode } from "../tools-helpers";
|
|
13
|
+
import { registerPrompts } from "../prompts";
|
|
14
|
+
import { startWebSocketServer } from "./websocket-server";
|
|
15
|
+
|
|
16
|
+
// Helper Functions
|
|
17
|
+
|
|
18
|
+
function getFigmaAccessToken(): string | undefined {
|
|
19
|
+
return process.env["FIGMA_PERSONAL_ACCESS_TOKEN"]?.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createFigmaClient(
|
|
23
|
+
serverUrl: string | undefined,
|
|
24
|
+
mode: ToolMode,
|
|
25
|
+
): FigmaWebSocketClient | null {
|
|
26
|
+
const pat = getFigmaAccessToken();
|
|
27
|
+
const resolvedUrl = serverUrl ?? "localhost";
|
|
28
|
+
|
|
29
|
+
switch (mode) {
|
|
30
|
+
case "rest": {
|
|
31
|
+
if (!pat) {
|
|
32
|
+
logger.warn(
|
|
33
|
+
"REST mode requires FIGMA_PERSONAL_ACCESS_TOKEN. Running without Figma client.",
|
|
34
|
+
);
|
|
35
|
+
} else {
|
|
36
|
+
logger.info("REST mode enabled. Using REST API only.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case "websocket": {
|
|
43
|
+
logger.info(`WebSocket mode enabled. Client connecting to: ${resolvedUrl}`);
|
|
44
|
+
|
|
45
|
+
return createFigmaWebSocketClient(resolvedUrl);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case "all": {
|
|
49
|
+
if (pat) {
|
|
50
|
+
logger.info(
|
|
51
|
+
"FIGMA_PERSONAL_ACCESS_TOKEN found. REST API enabled for figmaUrl/fileKey requests.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logger.info(`WebSocket client connecting to: ${resolvedUrl}`);
|
|
56
|
+
|
|
57
|
+
return createFigmaWebSocketClient(resolvedUrl);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createRestClient(mode: ToolMode): FigmaRestClient | null {
|
|
63
|
+
if (mode === "websocket") {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pat = getFigmaAccessToken();
|
|
68
|
+
if (!pat) {
|
|
69
|
+
if (mode === "rest") {
|
|
70
|
+
logger.warn(
|
|
71
|
+
"REST mode requires FIGMA_PERSONAL_ACCESS_TOKEN. REST API will not be available.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
logger.info("Initializing REST API client with PAT from environment");
|
|
79
|
+
|
|
80
|
+
return createFigmaRestClient(pat);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function loadMcpConfig(configPath?: string): Promise<McpConfig | null> {
|
|
84
|
+
if (!configPath) return null;
|
|
85
|
+
|
|
86
|
+
const config = await loadConfig(configPath);
|
|
87
|
+
if (!config) return null;
|
|
88
|
+
|
|
89
|
+
logger.info(`Loaded configuration from: ${configPath}`);
|
|
90
|
+
|
|
91
|
+
if (config.extend?.componentHandlers?.length) {
|
|
92
|
+
logger.info(`Found ${config.extend.componentHandlers.length} custom component handlers`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return config;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function connectFigmaClient(figmaClient: FigmaWebSocketClient | null): void {
|
|
99
|
+
if (!figmaClient) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
figmaClient.connectToFigma();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
logger.warn(`Could not connect to Figma initially: ${message}`);
|
|
106
|
+
|
|
107
|
+
if (getFigmaAccessToken()) {
|
|
108
|
+
logger.info("REST API fallback available via FIGMA_PERSONAL_ACCESS_TOKEN");
|
|
109
|
+
} else {
|
|
110
|
+
logger.warn("Will try to connect when the first command is sent");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MCP Server
|
|
116
|
+
|
|
117
|
+
interface McpServerOptions {
|
|
118
|
+
serverUrl?: string;
|
|
119
|
+
experimental?: boolean;
|
|
120
|
+
configPath?: string;
|
|
121
|
+
mode?: ToolMode;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function startMcpServer(options: McpServerOptions = {}): Promise<void> {
|
|
125
|
+
const { serverUrl, experimental, configPath, mode = "all" } = options;
|
|
126
|
+
|
|
127
|
+
const config = await loadMcpConfig(configPath);
|
|
128
|
+
const figmaClient = createFigmaClient(serverUrl, mode);
|
|
129
|
+
const restClient = createRestClient(mode);
|
|
130
|
+
|
|
131
|
+
const server = new McpServer({
|
|
132
|
+
name: "SEED Design MCP",
|
|
133
|
+
version,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
registerTools(server, figmaClient, restClient, config, mode);
|
|
137
|
+
registerPrompts(server);
|
|
138
|
+
|
|
139
|
+
if (experimental) {
|
|
140
|
+
if (mode === "rest") {
|
|
141
|
+
logger.warn("Experimental editing tools not available in REST mode. Skipping.");
|
|
142
|
+
} else if (figmaClient) {
|
|
143
|
+
registerEditingTools(server, figmaClient);
|
|
144
|
+
} else {
|
|
145
|
+
logger.warn("Experimental editing tools require WebSocket connection. Skipping.");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
connectFigmaClient(figmaClient);
|
|
150
|
+
|
|
151
|
+
const transport = new StdioServerTransport();
|
|
152
|
+
await server.connect(transport);
|
|
153
|
+
|
|
154
|
+
logger.info(`FigmaMCP server running on stdio (mode: ${mode})`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// CLI
|
|
158
|
+
|
|
159
|
+
const cli = cac("@seed-design/mcp");
|
|
160
|
+
|
|
161
|
+
cli
|
|
162
|
+
.command("", "Start the MCP server")
|
|
163
|
+
.option(
|
|
164
|
+
"--server <server>",
|
|
165
|
+
"WebSocket server URL. If not provided and FIGMA_PERSONAL_ACCESS_TOKEN is set, REST API mode will be used.",
|
|
166
|
+
)
|
|
167
|
+
.option("--experimental", "Enable experimental features", { default: false })
|
|
168
|
+
.option("--config <config>", "Path to configuration file (.js, .mjs, .ts, .mts)")
|
|
169
|
+
.option(
|
|
170
|
+
"--mode <mode>",
|
|
171
|
+
"Tool registration mode: 'rest' (REST API tools only), 'websocket' (WebSocket tools only), or 'all' (default)",
|
|
172
|
+
)
|
|
173
|
+
.action(async (options) => {
|
|
174
|
+
const mode = options.mode as ToolMode | undefined;
|
|
175
|
+
if (mode && !["rest", "websocket", "all"].includes(mode)) {
|
|
176
|
+
console.error(`Invalid mode: ${mode}. Use 'rest', 'websocket', or 'all'.`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await startMcpServer({
|
|
181
|
+
serverUrl: options.server,
|
|
182
|
+
experimental: options.experimental,
|
|
183
|
+
configPath: options.config,
|
|
184
|
+
mode,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
cli
|
|
189
|
+
.command("socket", "Start the WebSocket server")
|
|
190
|
+
.option("--port <port>", "Port number", { default: 3055 })
|
|
191
|
+
.action(async (options) => {
|
|
192
|
+
await startWebSocketServer(options.port);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
cli.help();
|
|
196
|
+
cli.version(version);
|
|
197
|
+
cli.parse();
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
|
|
3
|
+
interface JoinMessage {
|
|
4
|
+
type: "join";
|
|
5
|
+
channel: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ChatMessage {
|
|
10
|
+
type: "message";
|
|
11
|
+
channel: string;
|
|
12
|
+
message: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type WebSocketMessage = JoinMessage | ChatMessage;
|
|
16
|
+
|
|
17
|
+
const channels = new Map<string, Set<ServerWebSocket<unknown>>>();
|
|
18
|
+
|
|
19
|
+
function sendJson(ws: ServerWebSocket<unknown>, data: object): void {
|
|
20
|
+
ws.send(JSON.stringify(data));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function broadcastToChannel(
|
|
24
|
+
channelName: string,
|
|
25
|
+
message: object,
|
|
26
|
+
excludeWs?: ServerWebSocket<unknown>,
|
|
27
|
+
): void {
|
|
28
|
+
const clients = channels.get(channelName);
|
|
29
|
+
if (!clients) return;
|
|
30
|
+
|
|
31
|
+
for (const client of clients) {
|
|
32
|
+
if (client !== excludeWs && client.readyState === WebSocket.OPEN) {
|
|
33
|
+
sendJson(client, message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleJoin(ws: ServerWebSocket<unknown>, data: JoinMessage): void {
|
|
39
|
+
const { channel: channelName, id } = data;
|
|
40
|
+
|
|
41
|
+
if (!channelName || typeof channelName !== "string") {
|
|
42
|
+
sendJson(ws, { type: "error", message: "Channel name is required" });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!channels.has(channelName)) {
|
|
47
|
+
channels.set(channelName, new Set());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const channelClients = channels.get(channelName);
|
|
51
|
+
|
|
52
|
+
if (!channelClients) {
|
|
53
|
+
sendJson(ws, { type: "error", message: "Failed to join channel" });
|
|
54
|
+
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
channelClients.add(ws);
|
|
59
|
+
|
|
60
|
+
// Notify client they joined successfully
|
|
61
|
+
sendJson(ws, {
|
|
62
|
+
type: "system",
|
|
63
|
+
message: `Joined channel: ${channelName}`,
|
|
64
|
+
channel: channelName,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Send connection confirmation
|
|
68
|
+
console.log("Sending message to client:", id);
|
|
69
|
+
sendJson(ws, {
|
|
70
|
+
type: "system",
|
|
71
|
+
message: { id, result: `Connected to channel: ${channelName}` },
|
|
72
|
+
channel: channelName,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Notify other clients in channel
|
|
76
|
+
broadcastToChannel(
|
|
77
|
+
channelName,
|
|
78
|
+
{ type: "system", message: "A new user has joined the channel", channel: channelName },
|
|
79
|
+
ws,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handleMessage(ws: ServerWebSocket<unknown>, data: ChatMessage): void {
|
|
84
|
+
const { channel: channelName, message } = data;
|
|
85
|
+
|
|
86
|
+
if (!channelName || typeof channelName !== "string") {
|
|
87
|
+
sendJson(ws, { type: "error", message: "Channel name is required" });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const channelClients = channels.get(channelName);
|
|
92
|
+
if (!channelClients?.has(ws)) {
|
|
93
|
+
sendJson(ws, { type: "error", message: "You must join the channel first" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Broadcast to all clients in the channel
|
|
98
|
+
for (const client of channelClients) {
|
|
99
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
100
|
+
console.log("Broadcasting message to client:", message);
|
|
101
|
+
sendJson(client, {
|
|
102
|
+
type: "broadcast",
|
|
103
|
+
message,
|
|
104
|
+
sender: client === ws ? "You" : "User",
|
|
105
|
+
channel: channelName,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// WebSocket Event Handlers
|
|
112
|
+
|
|
113
|
+
function handleConnection(ws: ServerWebSocket<unknown>): void {
|
|
114
|
+
console.log("New client connected");
|
|
115
|
+
sendJson(ws, { type: "system", message: "Please join a channel to start chatting" });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleWebSocketMessage(ws: ServerWebSocket<unknown>, rawMessage: string | Buffer): void {
|
|
119
|
+
try {
|
|
120
|
+
console.log("Received message from client:", rawMessage);
|
|
121
|
+
const data = JSON.parse(rawMessage as string) as WebSocketMessage;
|
|
122
|
+
|
|
123
|
+
switch (data.type) {
|
|
124
|
+
case "join":
|
|
125
|
+
handleJoin(ws, data);
|
|
126
|
+
break;
|
|
127
|
+
case "message":
|
|
128
|
+
handleMessage(ws, data);
|
|
129
|
+
break;
|
|
130
|
+
default:
|
|
131
|
+
console.warn(`Unknown message type: ${(data as { type: string }).type}`);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error("Error handling message:", err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function handleClose(ws: ServerWebSocket<unknown>): void {
|
|
139
|
+
console.log("Client disconnected");
|
|
140
|
+
|
|
141
|
+
for (const [channelName, clients] of channels) {
|
|
142
|
+
if (clients.has(ws)) {
|
|
143
|
+
clients.delete(ws);
|
|
144
|
+
broadcastToChannel(channelName, {
|
|
145
|
+
type: "system",
|
|
146
|
+
message: "A user has left the channel",
|
|
147
|
+
channel: channelName,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Server
|
|
154
|
+
|
|
155
|
+
export async function startWebSocketServer(port: number) {
|
|
156
|
+
const server = Bun.serve({
|
|
157
|
+
port,
|
|
158
|
+
// uncomment this to allow connections in windows wsl
|
|
159
|
+
// hostname: "0.0.0.0",
|
|
160
|
+
fetch(req, server) {
|
|
161
|
+
// Handle CORS preflight
|
|
162
|
+
if (req.method === "OPTIONS") {
|
|
163
|
+
return new Response(null, {
|
|
164
|
+
headers: {
|
|
165
|
+
"Access-Control-Allow-Origin": "*",
|
|
166
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
167
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Handle WebSocket upgrade
|
|
173
|
+
if (server.upgrade(req, { headers: { "Access-Control-Allow-Origin": "*" }, data: {} })) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Return response for non-WebSocket requests
|
|
178
|
+
return new Response("WebSocket server running", {
|
|
179
|
+
headers: { "Access-Control-Allow-Origin": "*" },
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
websocket: {
|
|
184
|
+
open: handleConnection,
|
|
185
|
+
message: handleWebSocketMessage,
|
|
186
|
+
close: handleClose,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
console.log(`WebSocket server running on port ${server.port}`);
|
|
191
|
+
|
|
192
|
+
return server;
|
|
193
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CreatePipelineConfig } from "@seed-design/figma/codegen/targets/react";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { logger } from "./logger";
|
|
5
|
+
|
|
6
|
+
// Define config type
|
|
7
|
+
export interface McpConfig {
|
|
8
|
+
extend?: CreatePipelineConfig["extend"];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Config loader
|
|
12
|
+
export async function loadConfig(configPath: string) {
|
|
13
|
+
try {
|
|
14
|
+
const resolvedPath = path.resolve(process.cwd(), configPath);
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
17
|
+
logger.error(`Config file not found: ${resolvedPath}`);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Handle different file types
|
|
22
|
+
if (resolvedPath.endsWith(".json")) {
|
|
23
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
resolvedPath.endsWith(".js") ||
|
|
29
|
+
resolvedPath.endsWith(".mjs") ||
|
|
30
|
+
resolvedPath.endsWith(".ts") ||
|
|
31
|
+
resolvedPath.endsWith(".mts")
|
|
32
|
+
) {
|
|
33
|
+
// For JS/MJS/TS/MTS files, we can dynamically import with Bun
|
|
34
|
+
// Bun has built-in TypeScript support without requiring transpilation
|
|
35
|
+
const config = await import(resolvedPath);
|
|
36
|
+
return config.default || config;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logger.error(`Unsupported config file format: ${resolvedPath}`);
|
|
40
|
+
return null;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger.error(
|
|
43
|
+
`Failed to load config file: ${error instanceof Error ? error.message : String(error)}`,
|
|
44
|
+
);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Api as FigmaApi } from "figma-api";
|
|
2
|
+
import type { GetFileNodesResponse } from "@figma/rest-api-spec";
|
|
3
|
+
|
|
4
|
+
export interface FigmaRestClient {
|
|
5
|
+
getFileNodes(fileKey: string, nodeIds: string[]): Promise<GetFileNodesResponse>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createFigmaRestClient(personalAccessToken: string): FigmaRestClient {
|
|
9
|
+
const api = new FigmaApi({ personalAccessToken });
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
async getFileNodes(fileKey: string, nodeIds: string[]): Promise<GetFileNodesResponse> {
|
|
13
|
+
const response = await api.getFileNodes({ file_key: fileKey }, { ids: nodeIds.join(",") });
|
|
14
|
+
|
|
15
|
+
return response;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* https://www.figma.com/:file_type/:file_key/:file_name?node-id=:id
|
|
22
|
+
*
|
|
23
|
+
* file_type:
|
|
24
|
+
* - design
|
|
25
|
+
* - file (legacy)
|
|
26
|
+
*
|
|
27
|
+
* Note: While node-id is separated by hyphens ('-') in the URL,
|
|
28
|
+
* it must be converted to colons (':') when making API calls.
|
|
29
|
+
* e.g. URL "node-id=794-1987" → API "794:1987"
|
|
30
|
+
*/
|
|
31
|
+
export function parseFigmaUrl(url: string): { fileKey: string; nodeId: string } {
|
|
32
|
+
const __url: URL = (() => {
|
|
33
|
+
try {
|
|
34
|
+
return new URL(url);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
|
|
40
|
+
const pathMatch = __url.pathname.match(/^\/(design|file)\/([A-Za-z0-9]+)/);
|
|
41
|
+
|
|
42
|
+
const rawNodeId = __url.searchParams.get("node-id");
|
|
43
|
+
|
|
44
|
+
if (!pathMatch)
|
|
45
|
+
throw new Error(
|
|
46
|
+
"Invalid Figma URL: Expected format https://www.figma.com/design/{fileKey}/... or /file/{fileKey}/...",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (!rawNodeId) throw new Error("Invalid Figma URL: Missing node-id query parameter");
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
fileKey: pathMatch[2],
|
|
53
|
+
nodeId: rawNodeId.replace(/-/g, ":"),
|
|
54
|
+
};
|
|
55
|
+
}
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom logging module that writes to stderr instead of stdout
|
|
3
|
+
* to avoid being captured in MCP protocol communication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const logger = {
|
|
7
|
+
/**
|
|
8
|
+
* Log an informational message
|
|
9
|
+
*/
|
|
10
|
+
info: (message: string) => process.stderr.write(`[INFO] ${message}\n`),
|
|
11
|
+
|
|
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`),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format an error for logging
|
|
35
|
+
*/
|
|
36
|
+
export function formatError(error: unknown): string {
|
|
37
|
+
if (error instanceof Error) {
|
|
38
|
+
return error.message;
|
|
39
|
+
}
|
|
40
|
+
return String(error);
|
|
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 node:
|
|
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 implementing React components",
|
|
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
|
+
}
|
package/src/responses.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { formatError } from "./logger";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format an object response
|
|
6
|
+
*/
|
|
7
|
+
export function formatObjectResponse(result: unknown): CallToolResult {
|
|
8
|
+
return {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: "text",
|
|
12
|
+
text: JSON.stringify(result),
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format a text response
|
|
20
|
+
*/
|
|
21
|
+
export function formatTextResponse(text: string): CallToolResult {
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
text,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format an image response
|
|
34
|
+
*/
|
|
35
|
+
export function formatImageResponse(imageData: string, mimeType = "image/png"): CallToolResult {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "image",
|
|
40
|
+
data: imageData,
|
|
41
|
+
mimeType,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format an error response
|
|
49
|
+
*/
|
|
50
|
+
export function formatErrorResponse(toolName: string, error: unknown): CallToolResult {
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: `Error in ${toolName}: ${formatError(error)}`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format a progress response with initial message
|
|
63
|
+
*/
|
|
64
|
+
export function formatProgressResponse(initialMessage: string, result: unknown): CallToolResult {
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: initialMessage,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: typeof result === "string" ? result : JSON.stringify(result),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|