palette-mcp 1.0.0 → 1.1.0
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/dist/components/OrderManagementGuide.d.ts +6 -0
- package/dist/components/OrderManagementGuide.js +266 -0
- package/dist/index-simple.d.ts +2 -0
- package/dist/index-simple.js +139 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +16 -226
- package/dist/requests/1762927928451-ajgna9b/SomaComponent.html +63 -0
- package/dist/requests/1762927928451-ajgna9b/SomaComponent.tsx +913 -0
- package/dist/requests/1762927928451-ajgna9b/metadata.json +9 -0
- package/dist/requests/1762931214963-cqlwvxn/SomaComponent.html +63 -0
- package/dist/requests/1762931214963-cqlwvxn/SomaComponent.tsx +525 -0
- package/dist/requests/1762931214963-cqlwvxn/metadata.json +9 -0
- package/dist/requests/1762932805663-m5wkk3a/SomaComponent.html +248 -0
- package/dist/requests/1762932805663-m5wkk3a/SomaComponent.tsx +1050 -0
- package/dist/requests/1762932805663-m5wkk3a/metadata.json +9 -0
- package/dist/requests/1762934645710-b67ldow/SomaComponent.html +193 -0
- package/dist/requests/1762934645710-b67ldow/SomaComponent.tsx +307 -0
- package/dist/requests/1762934645710-b67ldow/metadata.json +9 -0
- package/dist/requests/1762934961257-wwnmpvx/SomaComponent.html +193 -0
- package/dist/requests/1762934961257-wwnmpvx/SomaComponent.tsx +932 -0
- package/dist/requests/1762934961257-wwnmpvx/metadata.json +9 -0
- package/dist/requests/1762935126549-yjdcezr/SomaComponent.html +193 -0
- package/dist/requests/1762935126549-yjdcezr/SomaComponent.tsx +847 -0
- package/dist/requests/1762935126549-yjdcezr/metadata.json +9 -0
- package/dist/requests/1762935353759-fuokdeu/SomaComponent.html +193 -0
- package/dist/requests/1762935353759-fuokdeu/SomaComponent.tsx +334 -0
- package/dist/requests/1762935353759-fuokdeu/metadata.json +9 -0
- package/dist/requests/1762935378891-ckwbabn/SomaComponent.html +193 -0
- package/dist/requests/1762935378891-ckwbabn/SomaComponent.tsx +256 -0
- package/dist/requests/1762935378891-ckwbabn/metadata.json +9 -0
- package/dist/requests/1762935418352-181zqu4/SomaComponent.html +193 -0
- package/dist/requests/1762935418352-181zqu4/SomaComponent.tsx +45 -0
- package/dist/requests/1762935418352-181zqu4/metadata.json +9 -0
- package/dist/requests/1762935438157-vzkcbwy/SomaComponent.html +193 -0
- package/dist/requests/1762935438157-vzkcbwy/SomaComponent.tsx +238 -0
- package/dist/requests/1762935438157-vzkcbwy/metadata.json +9 -0
- package/dist/requests/1762935529749-ukzmiu3/SomaComponent.html +193 -0
- package/dist/requests/1762935529749-ukzmiu3/SomaComponent.tsx +138 -0
- package/dist/requests/1762935529749-ukzmiu3/metadata.json +9 -0
- package/dist/requests/1762935556527-jxelwj4/SomaComponent.html +193 -0
- package/dist/requests/1762935556527-jxelwj4/SomaComponent.tsx +138 -0
- package/dist/requests/1762935556527-jxelwj4/metadata.json +9 -0
- package/dist/requests/1762935579673-g39fqly/SomaComponent.html +193 -0
- package/dist/requests/1762935579673-g39fqly/SomaComponent.tsx +138 -0
- package/dist/requests/1762935579673-g39fqly/metadata.json +9 -0
- package/dist/requests/1762935613556-ogvsekd/SomaComponent.html +193 -0
- package/dist/requests/1762935613556-ogvsekd/SomaComponent.tsx +150 -0
- package/dist/requests/1762935613556-ogvsekd/metadata.json +9 -0
- package/dist/requests/1762935943631-hb2drgf/SomaComponent.html +193 -0
- package/dist/requests/1762935943631-hb2drgf/SomaComponent.tsx +150 -0
- package/dist/requests/1762935943631-hb2drgf/metadata.json +9 -0
- package/dist/requests/1762935954110-m7jb9m7/SomaComponent.html +193 -0
- package/dist/requests/1762935954110-m7jb9m7/SomaComponent.tsx +150 -0
- package/dist/requests/1762935954110-m7jb9m7/metadata.json +9 -0
- package/dist/requests/1762936003095-0jnmlga/SomaComponent.html +193 -0
- package/dist/requests/1762936003095-0jnmlga/SomaComponent.tsx +150 -0
- package/dist/requests/1762936003095-0jnmlga/metadata.json +9 -0
- package/dist/requests/1762937044748-7ubrrua/SomaComponent.html +193 -0
- package/dist/requests/1762937044748-7ubrrua/SomaComponent.tsx +138 -0
- package/dist/requests/1762937044748-7ubrrua/metadata.json +9 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +246 -0
- package/dist/smithery.d.ts +50 -0
- package/dist/smithery.js +44 -0
- package/dist/test-figma-tools.d.ts +2 -0
- package/dist/test-figma-tools.js +55 -0
- package/dist/test-mcp-servers.d.ts +2 -0
- package/dist/test-mcp-servers.js +166 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.js +76 -0
- package/package.json +4 -5
- package/smithery.yaml +8 -57
- package/src/components/OrderManagementGuide.tsx +459 -0
- package/src/index.ts +43 -0
- package/src/server.ts +301 -0
- package/src/services/code-generator.ts +1330 -0
- package/src/services/design-system.ts +2133 -0
- package/src/services/figma.ts +422 -0
- package/src/smithery.ts +54 -0
- package/src/test-figma-tools.ts +72 -0
- package/src/test-mcp-servers.ts +180 -0
- package/src/test.ts +89 -0
- package/src/utils/figma-mcp-client.ts +193 -0
- package/src/utils/request-manager.ts +101 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { FigmaMCPClient } from '../utils/figma-mcp-client.js';
|
|
3
|
+
|
|
4
|
+
export interface FigmaNode {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
visible?: boolean;
|
|
9
|
+
children?: FigmaNode[];
|
|
10
|
+
absoluteBoundingBox?: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
fills?: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
color?: {
|
|
19
|
+
r: number;
|
|
20
|
+
g: number;
|
|
21
|
+
b: number;
|
|
22
|
+
a: number;
|
|
23
|
+
};
|
|
24
|
+
gradientStops?: Array<{
|
|
25
|
+
position: number;
|
|
26
|
+
color: {
|
|
27
|
+
r: number;
|
|
28
|
+
g: number;
|
|
29
|
+
b: number;
|
|
30
|
+
a: number;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
}>;
|
|
34
|
+
strokes?: Array<{
|
|
35
|
+
type: string;
|
|
36
|
+
color?: {
|
|
37
|
+
r: number;
|
|
38
|
+
g: number;
|
|
39
|
+
b: number;
|
|
40
|
+
a: number;
|
|
41
|
+
};
|
|
42
|
+
strokeWeight?: number;
|
|
43
|
+
}>;
|
|
44
|
+
cornerRadius?: number;
|
|
45
|
+
characters?: string;
|
|
46
|
+
style?: {
|
|
47
|
+
fontFamily?: string;
|
|
48
|
+
fontSize?: number;
|
|
49
|
+
fontWeight?: number;
|
|
50
|
+
textAlignHorizontal?: string;
|
|
51
|
+
textAlignVertical?: string;
|
|
52
|
+
};
|
|
53
|
+
layoutMode?: 'NONE' | 'HORIZONTAL' | 'VERTICAL';
|
|
54
|
+
primaryAxisSizingMode?: 'FIXED' | 'AUTO';
|
|
55
|
+
counterAxisSizingMode?: 'FIXED' | 'AUTO';
|
|
56
|
+
paddingLeft?: number;
|
|
57
|
+
paddingRight?: number;
|
|
58
|
+
paddingTop?: number;
|
|
59
|
+
paddingBottom?: number;
|
|
60
|
+
itemSpacing?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface FigmaFile {
|
|
64
|
+
document: FigmaNode;
|
|
65
|
+
components: Record<string, FigmaNode>;
|
|
66
|
+
styles: Record<string, any>;
|
|
67
|
+
name: string;
|
|
68
|
+
lastModified: string;
|
|
69
|
+
thumbnailUrl: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface FigmaAnalysis {
|
|
73
|
+
totalNodes: number;
|
|
74
|
+
componentCount: number;
|
|
75
|
+
frameCount: number;
|
|
76
|
+
textCount: number;
|
|
77
|
+
availableComponents: string[];
|
|
78
|
+
suggestedMappings: Array<{
|
|
79
|
+
figmaComponent: string;
|
|
80
|
+
designSystemComponent: string;
|
|
81
|
+
confidence: number;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class FigmaService {
|
|
86
|
+
private accessToken: string;
|
|
87
|
+
private baseUrl = 'https://api.figma.com/v1';
|
|
88
|
+
private mcpClient: FigmaMCPClient | null = null;
|
|
89
|
+
private useMCP: boolean;
|
|
90
|
+
|
|
91
|
+
constructor(useMCP: boolean = true, mcpBaseUrl?: string) {
|
|
92
|
+
this.accessToken = process.env.FIGMA_ACCESS_TOKEN || '';
|
|
93
|
+
this.useMCP = useMCP;
|
|
94
|
+
|
|
95
|
+
if (useMCP) {
|
|
96
|
+
const mcpUrl = mcpBaseUrl || process.env.FIGMA_MCP_SERVER_URL || 'http://127.0.0.1:3845/mcp';
|
|
97
|
+
this.mcpClient = new FigmaMCPClient(mcpUrl);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!this.accessToken) {
|
|
101
|
+
console.warn('환경 변수에서 FIGMA_ACCESS_TOKEN을 찾을 수 없습니다.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Figma URL에서 파일 ID 추출
|
|
107
|
+
*/
|
|
108
|
+
private extractFileId(url: string): string {
|
|
109
|
+
// /file/ 또는 /design/ 경로에서 파일 ID 추출
|
|
110
|
+
const match = url.match(/\/(?:file|design)\/([a-zA-Z0-9]+)/);
|
|
111
|
+
if (match) {
|
|
112
|
+
return match[1];
|
|
113
|
+
}
|
|
114
|
+
// 이미 파일 ID인 경우
|
|
115
|
+
if (/^[a-zA-Z0-9]+$/.test(url)) {
|
|
116
|
+
return url;
|
|
117
|
+
}
|
|
118
|
+
throw new Error('잘못된 Figma URL 형식입니다.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Figma URL에서 node-id 추출
|
|
123
|
+
*/
|
|
124
|
+
extractNodeId(url: string): string | undefined {
|
|
125
|
+
const match = url.match(/[?&]node-id=([^&]+)/);
|
|
126
|
+
if (match) {
|
|
127
|
+
// node-id는 URL 인코딩되어 있을 수 있으므로 디코딩
|
|
128
|
+
return decodeURIComponent(match[1]);
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* MCP 응답을 FigmaFile 형식으로 변환
|
|
135
|
+
*/
|
|
136
|
+
private transformMCPResponseToFigmaFile(mcpData: any): FigmaFile {
|
|
137
|
+
// MCP 응답 형식에 따라 변환 로직 구현
|
|
138
|
+
// Figma MCP 서버의 응답 구조에 맞게 조정 필요
|
|
139
|
+
if (!mcpData) {
|
|
140
|
+
throw new Error('MCP 응답 데이터가 없습니다.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// MCP 응답이 이미 FigmaFile 형식인 경우
|
|
144
|
+
if (mcpData.document) {
|
|
145
|
+
return {
|
|
146
|
+
document: mcpData.document,
|
|
147
|
+
components: mcpData.components || {},
|
|
148
|
+
styles: mcpData.styles || {},
|
|
149
|
+
name: mcpData.name || 'Untitled',
|
|
150
|
+
lastModified: mcpData.lastModified || new Date().toISOString(),
|
|
151
|
+
thumbnailUrl: mcpData.thumbnailUrl || '',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// MCP 응답이 다른 형식인 경우 변환
|
|
156
|
+
// content 배열에서 데이터 추출 (MCP 도구 응답 형식)
|
|
157
|
+
if (mcpData.content && Array.isArray(mcpData.content)) {
|
|
158
|
+
const textContent = mcpData.content.find((c: any) => c.type === 'text');
|
|
159
|
+
if (textContent) {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(textContent.text);
|
|
162
|
+
return this.transformMCPResponseToFigmaFile(parsed);
|
|
163
|
+
} catch {
|
|
164
|
+
// JSON이 아닌 경우 처리
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 기본 구조로 변환 시도
|
|
170
|
+
return {
|
|
171
|
+
document: mcpData.document || mcpData.node || { id: 'root', name: 'Document', type: 'DOCUMENT', children: [] },
|
|
172
|
+
components: mcpData.components || {},
|
|
173
|
+
styles: mcpData.styles || {},
|
|
174
|
+
name: mcpData.name || 'Untitled',
|
|
175
|
+
lastModified: mcpData.lastModified || new Date().toISOString(),
|
|
176
|
+
thumbnailUrl: mcpData.thumbnailUrl || '',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Figma 파일 데이터 가져오기
|
|
182
|
+
* MCP 서버를 우선 사용하고, 실패 시 기존 REST API로 폴백
|
|
183
|
+
*/
|
|
184
|
+
async getFigmaData(url: string, nodeId?: string): Promise<FigmaFile> {
|
|
185
|
+
const fileId = this.extractFileId(url);
|
|
186
|
+
|
|
187
|
+
// MCP 클라이언트 사용 시도
|
|
188
|
+
if (this.useMCP && this.mcpClient !== null) {
|
|
189
|
+
try {
|
|
190
|
+
const isAvailable = await this.mcpClient.isAvailable();
|
|
191
|
+
if (isAvailable) {
|
|
192
|
+
const mcpData = nodeId
|
|
193
|
+
? await this.mcpClient.getNodeData(fileId, nodeId)
|
|
194
|
+
: await this.mcpClient.getFileData(fileId, nodeId);
|
|
195
|
+
|
|
196
|
+
if (mcpData) {
|
|
197
|
+
try {
|
|
198
|
+
return this.transformMCPResponseToFigmaFile(mcpData);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.warn('MCP 응답 변환 실패, REST API로 폴백:', error);
|
|
201
|
+
// 폴백으로 REST API 사용
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.warn('Figma MCP 서버 연결 실패, REST API로 폴백:', error);
|
|
207
|
+
// 폴백으로 REST API 사용
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// REST API 폴백
|
|
212
|
+
if (!this.accessToken) {
|
|
213
|
+
throw new Error('Figma 액세스 토큰이 필요합니다. MCP 서버도 사용할 수 없습니다.');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const response = await axios.get(`${this.baseUrl}/files/${fileId}`, {
|
|
218
|
+
headers: {
|
|
219
|
+
'X-Figma-Token': this.accessToken,
|
|
220
|
+
},
|
|
221
|
+
params: {
|
|
222
|
+
ids: nodeId || undefined,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
document: response.data.document,
|
|
228
|
+
components: response.data.components || {},
|
|
229
|
+
styles: response.data.styles || {},
|
|
230
|
+
name: response.data.name,
|
|
231
|
+
lastModified: response.data.lastModified,
|
|
232
|
+
thumbnailUrl: response.data.thumbnailUrl,
|
|
233
|
+
};
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (axios.isAxiosError(error)) {
|
|
236
|
+
throw new Error(`Figma API error: ${error.response?.data?.message || error.message}`);
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Figma 파일 구조 분석
|
|
244
|
+
*/
|
|
245
|
+
async analyzeFigmaFile(url: string): Promise<string> {
|
|
246
|
+
const fileData = await this.getFigmaData(url);
|
|
247
|
+
|
|
248
|
+
const analysis = this.analyzeFileStructure(fileData);
|
|
249
|
+
|
|
250
|
+
return this.formatAnalysis(analysis);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 파일 구조 분석 및 컴포넌트 정보 추출
|
|
255
|
+
*/
|
|
256
|
+
private analyzeFileStructure(file: FigmaFile): FigmaAnalysis {
|
|
257
|
+
const stats = this.countNodes(file.document);
|
|
258
|
+
const availableComponents = Object.keys(file.components);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
totalNodes: stats.total,
|
|
262
|
+
componentCount: stats.components,
|
|
263
|
+
frameCount: stats.frames,
|
|
264
|
+
textCount: stats.text,
|
|
265
|
+
availableComponents,
|
|
266
|
+
suggestedMappings: this.suggestComponentMappings(availableComponents),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 파일에 있는 다른 유형의 노드 수 세기
|
|
272
|
+
*/
|
|
273
|
+
private countNodes(node: FigmaNode): {
|
|
274
|
+
total: number;
|
|
275
|
+
components: number;
|
|
276
|
+
frames: number;
|
|
277
|
+
text: number;
|
|
278
|
+
} {
|
|
279
|
+
let total = 1;
|
|
280
|
+
let components = node.type === 'COMPONENT' ? 1 : 0;
|
|
281
|
+
let frames = node.type === 'FRAME' ? 1 : 0;
|
|
282
|
+
let text = node.type === 'TEXT' ? 1 : 0;
|
|
283
|
+
|
|
284
|
+
if (node.children) {
|
|
285
|
+
for (const child of node.children) {
|
|
286
|
+
const childStats = this.countNodes(child);
|
|
287
|
+
total += childStats.total;
|
|
288
|
+
components += childStats.components;
|
|
289
|
+
frames += childStats.frames;
|
|
290
|
+
text += childStats.text;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { total, components, frames, text };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Figma 컴포넌트와 디자인 시스템 컴포넌트 간의 매핑 제안
|
|
299
|
+
*/
|
|
300
|
+
private suggestComponentMappings(figmaComponents: string[]): Array<{
|
|
301
|
+
figmaComponent: string;
|
|
302
|
+
designSystemComponent: string;
|
|
303
|
+
confidence: number;
|
|
304
|
+
}> {
|
|
305
|
+
const mappings: Array<{
|
|
306
|
+
figmaComponent: string;
|
|
307
|
+
designSystemComponent: string;
|
|
308
|
+
confidence: number;
|
|
309
|
+
}> = [];
|
|
310
|
+
|
|
311
|
+
// 일반적인 컴포넌트 이름 매핑
|
|
312
|
+
const commonMappings: Record<string, string[]> = {
|
|
313
|
+
'button': ['Button', 'Btn', 'PrimaryButton', 'SecondaryButton'],
|
|
314
|
+
'input': ['Input', 'TextField', 'TextInput'],
|
|
315
|
+
'card': ['Card', 'Panel', 'Container'],
|
|
316
|
+
'modal': ['Modal', 'Dialog', 'Popup'],
|
|
317
|
+
'header': ['Header', 'Navbar', 'Navigation'],
|
|
318
|
+
'footer': ['Footer', 'BottomBar'],
|
|
319
|
+
'sidebar': ['Sidebar', 'Drawer', 'Navigation'],
|
|
320
|
+
'form': ['Form', 'FormGroup'],
|
|
321
|
+
'table': ['Table', 'DataTable'],
|
|
322
|
+
'list': ['List', 'ListItem'],
|
|
323
|
+
'avatar': ['Avatar', 'ProfileImage'],
|
|
324
|
+
'badge': ['Badge', 'Tag', 'Label'],
|
|
325
|
+
'tooltip': ['Tooltip', 'Popover'],
|
|
326
|
+
'dropdown': ['Dropdown', 'Select'],
|
|
327
|
+
'checkbox': ['Checkbox', 'CheckBox'],
|
|
328
|
+
'radio': ['Radio', 'RadioButton'],
|
|
329
|
+
'switch': ['Switch', 'Toggle'],
|
|
330
|
+
'slider': ['Slider', 'Range'],
|
|
331
|
+
'progress': ['Progress', 'ProgressBar'],
|
|
332
|
+
'spinner': ['Spinner', 'Loader'],
|
|
333
|
+
'alert': ['Alert', 'Notification'],
|
|
334
|
+
'breadcrumb': ['Breadcrumb', 'Breadcrumbs'],
|
|
335
|
+
'pagination': ['Pagination', 'Pager'],
|
|
336
|
+
'tabs': ['Tabs', 'TabList'],
|
|
337
|
+
'accordion': ['Accordion', 'Collapse'],
|
|
338
|
+
'carousel': ['Carousel', 'Slider'],
|
|
339
|
+
'stepper': ['Stepper', 'Steps'],
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
for (const figmaComponent of figmaComponents) {
|
|
343
|
+
const lowerName = figmaComponent.toLowerCase();
|
|
344
|
+
|
|
345
|
+
for (const [key, possibleNames] of Object.entries(commonMappings)) {
|
|
346
|
+
for (const possibleName of possibleNames) {
|
|
347
|
+
if (lowerName.includes(key) || lowerName.includes(possibleName.toLowerCase())) {
|
|
348
|
+
mappings.push({
|
|
349
|
+
figmaComponent,
|
|
350
|
+
designSystemComponent: possibleName,
|
|
351
|
+
confidence: 0.8,
|
|
352
|
+
});
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return mappings;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 분석 결과 표시 형식 지정
|
|
364
|
+
*/
|
|
365
|
+
private formatAnalysis(analysis: FigmaAnalysis): string {
|
|
366
|
+
let result = `## Figma File Analysis\n\n`;
|
|
367
|
+
result += `**File Statistics:**\n`;
|
|
368
|
+
result += `- Total Nodes: ${analysis.totalNodes}\n`;
|
|
369
|
+
result += `- Components: ${analysis.componentCount}\n`;
|
|
370
|
+
result += `- Frames: ${analysis.frameCount}\n`;
|
|
371
|
+
result += `- Text Elements: ${analysis.textCount}\n\n`;
|
|
372
|
+
|
|
373
|
+
if (analysis.availableComponents.length > 0) {
|
|
374
|
+
result += `**Available Components:**\n`;
|
|
375
|
+
analysis.availableComponents.forEach(comp => {
|
|
376
|
+
result += `- ${comp}\n`;
|
|
377
|
+
});
|
|
378
|
+
result += `\n`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (analysis.suggestedMappings.length > 0) {
|
|
382
|
+
result += `**Suggested Component Mappings:**\n`;
|
|
383
|
+
analysis.suggestedMappings.forEach(mapping => {
|
|
384
|
+
result += `- ${mapping.figmaComponent} → ${mapping.designSystemComponent} (${Math.round(mapping.confidence * 100)}% confidence)\n`;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Figma 파일에서 디자인 토큰 추출
|
|
393
|
+
*/
|
|
394
|
+
extractDesignTokens(file: FigmaFile): Record<string, any> {
|
|
395
|
+
const tokens: Record<string, any> = {
|
|
396
|
+
colors: {},
|
|
397
|
+
typography: {},
|
|
398
|
+
spacing: {},
|
|
399
|
+
borderRadius: {},
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// 스타일에서 색상 추출
|
|
403
|
+
for (const [key, style] of Object.entries(file.styles)) {
|
|
404
|
+
if (style.styleType === 'FILL') {
|
|
405
|
+
tokens.colors[key] = style.description || key;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 타이포그래피 추출
|
|
410
|
+
for (const [key, style] of Object.entries(file.styles)) {
|
|
411
|
+
if (style.styleType === 'TEXT') {
|
|
412
|
+
tokens.typography[key] = {
|
|
413
|
+
fontFamily: style.fontFamily,
|
|
414
|
+
fontSize: style.fontSize,
|
|
415
|
+
fontWeight: style.fontWeight,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return tokens;
|
|
421
|
+
}
|
|
422
|
+
}
|
package/src/smithery.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Palette MCP Server - Smithery Remote 배포용
|
|
3
|
+
*
|
|
4
|
+
* Smithery.ai에서 호스팅될 때 사용됩니다.
|
|
5
|
+
* Smithery가 이 파일을 로드하고 createServer 함수를 호출합니다.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { createPaletteServer, tools } from './server.js';
|
|
10
|
+
|
|
11
|
+
// Smithery 설정 스키마 정의
|
|
12
|
+
export const configSchema = z.object({
|
|
13
|
+
FIGMA_ACCESS_TOKEN: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('Figma Personal Access Token (https://www.figma.com/developers/api#access-tokens)'),
|
|
16
|
+
GITHUB_TOKEN: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe('GitHub Personal Access Token for design system packages'),
|
|
19
|
+
FIGMA_MCP_SERVER_URL: z
|
|
20
|
+
.string()
|
|
21
|
+
.default('http://127.0.0.1:3845/mcp')
|
|
22
|
+
.describe('Figma Dev Mode MCP server URL'),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Smithery 설정 타입
|
|
26
|
+
type SmitheryConfig = z.infer<typeof configSchema>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Smithery에서 호출하는 서버 생성 함수
|
|
30
|
+
*
|
|
31
|
+
* @param config - Smithery에서 전달받은 사용자 설정
|
|
32
|
+
* @returns MCP 서버 인스턴스
|
|
33
|
+
*/
|
|
34
|
+
export default function createServer({ config }: { config: SmitheryConfig }) {
|
|
35
|
+
// 환경변수 설정
|
|
36
|
+
process.env.FIGMA_ACCESS_TOKEN = config.FIGMA_ACCESS_TOKEN;
|
|
37
|
+
process.env.GITHUB_TOKEN = config.GITHUB_TOKEN;
|
|
38
|
+
process.env.FIGMA_MCP_SERVER_URL = config.FIGMA_MCP_SERVER_URL;
|
|
39
|
+
|
|
40
|
+
// 공통 서버 생성 로직 사용
|
|
41
|
+
const server = createPaletteServer({
|
|
42
|
+
figmaAccessToken: config.FIGMA_ACCESS_TOKEN,
|
|
43
|
+
githubToken: config.GITHUB_TOKEN,
|
|
44
|
+
figmaMcpServerUrl: config.FIGMA_MCP_SERVER_URL,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
console.error('Palette server created for Smithery (Remote mode)');
|
|
48
|
+
|
|
49
|
+
// Smithery가 기대하는 형식으로 반환
|
|
50
|
+
return server;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Tools 정보도 export (Smithery가 capabilities 탐지에 사용할 수 있음)
|
|
54
|
+
export { tools };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
dotenv.config({ path: join(__dirname, '..', '.env') });
|
|
11
|
+
|
|
12
|
+
import { FigmaMCPClient } from './utils/figma-mcp-client.js';
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const client = new FigmaMCPClient();
|
|
16
|
+
|
|
17
|
+
console.log('Figma Desktop MCP 서버에 연결 중...\n');
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const isAvailable = await client.isAvailable();
|
|
21
|
+
if (!isAvailable) {
|
|
22
|
+
console.error('❌ Figma Desktop MCP 서버를 사용할 수 없습니다.');
|
|
23
|
+
console.log('Figma Desktop 앱이 실행 중이고 MCP 서버가 활성화되어 있는지 확인하세요.');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log('✅ Figma Desktop MCP 서버 연결 성공!\n');
|
|
28
|
+
|
|
29
|
+
console.log('도구 목록 조회 중...');
|
|
30
|
+
|
|
31
|
+
// 직접 sendRequest를 호출해서 원본 응답 확인
|
|
32
|
+
// @ts-ignore - private 메서드이지만 테스트를 위해 접근
|
|
33
|
+
const rawResult = await (client as any).sendRequest('tools/list');
|
|
34
|
+
console.log('원본 응답:', JSON.stringify(rawResult, null, 2));
|
|
35
|
+
|
|
36
|
+
const tools = await client.listTools();
|
|
37
|
+
|
|
38
|
+
console.log(`파싱된 도구 목록:`, JSON.stringify(tools, null, 2));
|
|
39
|
+
|
|
40
|
+
if (tools.length === 0) {
|
|
41
|
+
console.log('⚠️ 사용 가능한 도구가 없습니다.');
|
|
42
|
+
console.log('\n💡 확인 사항:');
|
|
43
|
+
console.log('1. Figma Desktop 앱이 실행 중인지 확인하세요.');
|
|
44
|
+
console.log('2. Figma Desktop 앱의 Preferences에서 "Enable Dev Mode MCP Server"가 활성화되어 있는지 확인하세요.');
|
|
45
|
+
console.log('3. Figma 파일이 열려있는지 확인하세요.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`📋 총 ${tools.length}개의 도구를 찾았습니다:\n`);
|
|
50
|
+
|
|
51
|
+
tools.forEach((tool: any, index: number) => {
|
|
52
|
+
console.log(`${index + 1}. ${tool.name}`);
|
|
53
|
+
if (tool.description) {
|
|
54
|
+
console.log(` 설명: ${tool.description}`);
|
|
55
|
+
}
|
|
56
|
+
if (tool.inputSchema?.properties) {
|
|
57
|
+
console.log(` 파라미터:`);
|
|
58
|
+
Object.entries(tool.inputSchema.properties).forEach(([key, value]: [string, any]) => {
|
|
59
|
+
console.log(` - ${key}: ${value.type || 'any'} ${value.description ? `(${value.description})` : ''}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
console.log('');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('❌ 오류 발생:', error);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main();
|
|
72
|
+
|