palette-mcp 1.0.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/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +244 -0
- package/dist/services/code-generator.d.ts +121 -0
- package/dist/services/code-generator.js +1073 -0
- package/dist/services/design-system.d.ts +53 -0
- package/dist/services/design-system.js +2093 -0
- package/dist/services/figma.d.ts +126 -0
- package/dist/services/figma.js +295 -0
- package/dist/utils/figma-mcp-client.d.ts +40 -0
- package/dist/utils/figma-mcp-client.js +171 -0
- package/dist/utils/request-manager.d.ts +36 -0
- package/dist/utils/request-manager.js +62 -0
- package/package.json +93 -0
- package/smithery.yaml +61 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export interface FigmaNode {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
visible?: boolean;
|
|
6
|
+
children?: FigmaNode[];
|
|
7
|
+
absoluteBoundingBox?: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
fills?: Array<{
|
|
14
|
+
type: string;
|
|
15
|
+
color?: {
|
|
16
|
+
r: number;
|
|
17
|
+
g: number;
|
|
18
|
+
b: number;
|
|
19
|
+
a: number;
|
|
20
|
+
};
|
|
21
|
+
gradientStops?: Array<{
|
|
22
|
+
position: number;
|
|
23
|
+
color: {
|
|
24
|
+
r: number;
|
|
25
|
+
g: number;
|
|
26
|
+
b: number;
|
|
27
|
+
a: number;
|
|
28
|
+
};
|
|
29
|
+
}>;
|
|
30
|
+
}>;
|
|
31
|
+
strokes?: Array<{
|
|
32
|
+
type: string;
|
|
33
|
+
color?: {
|
|
34
|
+
r: number;
|
|
35
|
+
g: number;
|
|
36
|
+
b: number;
|
|
37
|
+
a: number;
|
|
38
|
+
};
|
|
39
|
+
strokeWeight?: number;
|
|
40
|
+
}>;
|
|
41
|
+
cornerRadius?: number;
|
|
42
|
+
characters?: string;
|
|
43
|
+
style?: {
|
|
44
|
+
fontFamily?: string;
|
|
45
|
+
fontSize?: number;
|
|
46
|
+
fontWeight?: number;
|
|
47
|
+
textAlignHorizontal?: string;
|
|
48
|
+
textAlignVertical?: string;
|
|
49
|
+
};
|
|
50
|
+
layoutMode?: 'NONE' | 'HORIZONTAL' | 'VERTICAL';
|
|
51
|
+
primaryAxisSizingMode?: 'FIXED' | 'AUTO';
|
|
52
|
+
counterAxisSizingMode?: 'FIXED' | 'AUTO';
|
|
53
|
+
paddingLeft?: number;
|
|
54
|
+
paddingRight?: number;
|
|
55
|
+
paddingTop?: number;
|
|
56
|
+
paddingBottom?: number;
|
|
57
|
+
itemSpacing?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface FigmaFile {
|
|
60
|
+
document: FigmaNode;
|
|
61
|
+
components: Record<string, FigmaNode>;
|
|
62
|
+
styles: Record<string, any>;
|
|
63
|
+
name: string;
|
|
64
|
+
lastModified: string;
|
|
65
|
+
thumbnailUrl: string;
|
|
66
|
+
}
|
|
67
|
+
export interface FigmaAnalysis {
|
|
68
|
+
totalNodes: number;
|
|
69
|
+
componentCount: number;
|
|
70
|
+
frameCount: number;
|
|
71
|
+
textCount: number;
|
|
72
|
+
availableComponents: string[];
|
|
73
|
+
suggestedMappings: Array<{
|
|
74
|
+
figmaComponent: string;
|
|
75
|
+
designSystemComponent: string;
|
|
76
|
+
confidence: number;
|
|
77
|
+
}>;
|
|
78
|
+
}
|
|
79
|
+
export declare class FigmaService {
|
|
80
|
+
private accessToken;
|
|
81
|
+
private baseUrl;
|
|
82
|
+
private mcpClient;
|
|
83
|
+
private useMCP;
|
|
84
|
+
constructor(useMCP?: boolean, mcpBaseUrl?: string);
|
|
85
|
+
/**
|
|
86
|
+
* Figma URL에서 파일 ID 추출
|
|
87
|
+
*/
|
|
88
|
+
private extractFileId;
|
|
89
|
+
/**
|
|
90
|
+
* Figma URL에서 node-id 추출
|
|
91
|
+
*/
|
|
92
|
+
extractNodeId(url: string): string | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* MCP 응답을 FigmaFile 형식으로 변환
|
|
95
|
+
*/
|
|
96
|
+
private transformMCPResponseToFigmaFile;
|
|
97
|
+
/**
|
|
98
|
+
* Figma 파일 데이터 가져오기
|
|
99
|
+
* MCP 서버를 우선 사용하고, 실패 시 기존 REST API로 폴백
|
|
100
|
+
*/
|
|
101
|
+
getFigmaData(url: string, nodeId?: string): Promise<FigmaFile>;
|
|
102
|
+
/**
|
|
103
|
+
* Figma 파일 구조 분석
|
|
104
|
+
*/
|
|
105
|
+
analyzeFigmaFile(url: string): Promise<string>;
|
|
106
|
+
/**
|
|
107
|
+
* 파일 구조 분석 및 컴포넌트 정보 추출
|
|
108
|
+
*/
|
|
109
|
+
private analyzeFileStructure;
|
|
110
|
+
/**
|
|
111
|
+
* 파일에 있는 다른 유형의 노드 수 세기
|
|
112
|
+
*/
|
|
113
|
+
private countNodes;
|
|
114
|
+
/**
|
|
115
|
+
* Figma 컴포넌트와 디자인 시스템 컴포넌트 간의 매핑 제안
|
|
116
|
+
*/
|
|
117
|
+
private suggestComponentMappings;
|
|
118
|
+
/**
|
|
119
|
+
* 분석 결과 표시 형식 지정
|
|
120
|
+
*/
|
|
121
|
+
private formatAnalysis;
|
|
122
|
+
/**
|
|
123
|
+
* Figma 파일에서 디자인 토큰 추출
|
|
124
|
+
*/
|
|
125
|
+
extractDesignTokens(file: FigmaFile): Record<string, any>;
|
|
126
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { FigmaMCPClient } from '../utils/figma-mcp-client.js';
|
|
3
|
+
export class FigmaService {
|
|
4
|
+
accessToken;
|
|
5
|
+
baseUrl = 'https://api.figma.com/v1';
|
|
6
|
+
mcpClient = null;
|
|
7
|
+
useMCP;
|
|
8
|
+
constructor(useMCP = true, mcpBaseUrl) {
|
|
9
|
+
this.accessToken = process.env.FIGMA_ACCESS_TOKEN || '';
|
|
10
|
+
this.useMCP = useMCP;
|
|
11
|
+
if (useMCP) {
|
|
12
|
+
const mcpUrl = mcpBaseUrl || process.env.FIGMA_MCP_SERVER_URL || 'http://127.0.0.1:3845/mcp';
|
|
13
|
+
this.mcpClient = new FigmaMCPClient(mcpUrl);
|
|
14
|
+
}
|
|
15
|
+
if (!this.accessToken) {
|
|
16
|
+
console.warn('환경 변수에서 FIGMA_ACCESS_TOKEN을 찾을 수 없습니다.');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Figma URL에서 파일 ID 추출
|
|
21
|
+
*/
|
|
22
|
+
extractFileId(url) {
|
|
23
|
+
// /file/ 또는 /design/ 경로에서 파일 ID 추출
|
|
24
|
+
const match = url.match(/\/(?:file|design)\/([a-zA-Z0-9]+)/);
|
|
25
|
+
if (match) {
|
|
26
|
+
return match[1];
|
|
27
|
+
}
|
|
28
|
+
// 이미 파일 ID인 경우
|
|
29
|
+
if (/^[a-zA-Z0-9]+$/.test(url)) {
|
|
30
|
+
return url;
|
|
31
|
+
}
|
|
32
|
+
throw new Error('잘못된 Figma URL 형식입니다.');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Figma URL에서 node-id 추출
|
|
36
|
+
*/
|
|
37
|
+
extractNodeId(url) {
|
|
38
|
+
const match = url.match(/[?&]node-id=([^&]+)/);
|
|
39
|
+
if (match) {
|
|
40
|
+
// node-id는 URL 인코딩되어 있을 수 있으므로 디코딩
|
|
41
|
+
return decodeURIComponent(match[1]);
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* MCP 응답을 FigmaFile 형식으로 변환
|
|
47
|
+
*/
|
|
48
|
+
transformMCPResponseToFigmaFile(mcpData) {
|
|
49
|
+
// MCP 응답 형식에 따라 변환 로직 구현
|
|
50
|
+
// Figma MCP 서버의 응답 구조에 맞게 조정 필요
|
|
51
|
+
if (!mcpData) {
|
|
52
|
+
throw new Error('MCP 응답 데이터가 없습니다.');
|
|
53
|
+
}
|
|
54
|
+
// MCP 응답이 이미 FigmaFile 형식인 경우
|
|
55
|
+
if (mcpData.document) {
|
|
56
|
+
return {
|
|
57
|
+
document: mcpData.document,
|
|
58
|
+
components: mcpData.components || {},
|
|
59
|
+
styles: mcpData.styles || {},
|
|
60
|
+
name: mcpData.name || 'Untitled',
|
|
61
|
+
lastModified: mcpData.lastModified || new Date().toISOString(),
|
|
62
|
+
thumbnailUrl: mcpData.thumbnailUrl || '',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// MCP 응답이 다른 형식인 경우 변환
|
|
66
|
+
// content 배열에서 데이터 추출 (MCP 도구 응답 형식)
|
|
67
|
+
if (mcpData.content && Array.isArray(mcpData.content)) {
|
|
68
|
+
const textContent = mcpData.content.find((c) => c.type === 'text');
|
|
69
|
+
if (textContent) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(textContent.text);
|
|
72
|
+
return this.transformMCPResponseToFigmaFile(parsed);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// JSON이 아닌 경우 처리
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 기본 구조로 변환 시도
|
|
80
|
+
return {
|
|
81
|
+
document: mcpData.document || mcpData.node || { id: 'root', name: 'Document', type: 'DOCUMENT', children: [] },
|
|
82
|
+
components: mcpData.components || {},
|
|
83
|
+
styles: mcpData.styles || {},
|
|
84
|
+
name: mcpData.name || 'Untitled',
|
|
85
|
+
lastModified: mcpData.lastModified || new Date().toISOString(),
|
|
86
|
+
thumbnailUrl: mcpData.thumbnailUrl || '',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Figma 파일 데이터 가져오기
|
|
91
|
+
* MCP 서버를 우선 사용하고, 실패 시 기존 REST API로 폴백
|
|
92
|
+
*/
|
|
93
|
+
async getFigmaData(url, nodeId) {
|
|
94
|
+
const fileId = this.extractFileId(url);
|
|
95
|
+
// MCP 클라이언트 사용 시도
|
|
96
|
+
if (this.useMCP && this.mcpClient !== null) {
|
|
97
|
+
try {
|
|
98
|
+
const isAvailable = await this.mcpClient.isAvailable();
|
|
99
|
+
if (isAvailable) {
|
|
100
|
+
const mcpData = nodeId
|
|
101
|
+
? await this.mcpClient.getNodeData(fileId, nodeId)
|
|
102
|
+
: await this.mcpClient.getFileData(fileId, nodeId);
|
|
103
|
+
if (mcpData) {
|
|
104
|
+
try {
|
|
105
|
+
return this.transformMCPResponseToFigmaFile(mcpData);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.warn('MCP 응답 변환 실패, REST API로 폴백:', error);
|
|
109
|
+
// 폴백으로 REST API 사용
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.warn('Figma MCP 서버 연결 실패, REST API로 폴백:', error);
|
|
116
|
+
// 폴백으로 REST API 사용
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// REST API 폴백
|
|
120
|
+
if (!this.accessToken) {
|
|
121
|
+
throw new Error('Figma 액세스 토큰이 필요합니다. MCP 서버도 사용할 수 없습니다.');
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const response = await axios.get(`${this.baseUrl}/files/${fileId}`, {
|
|
125
|
+
headers: {
|
|
126
|
+
'X-Figma-Token': this.accessToken,
|
|
127
|
+
},
|
|
128
|
+
params: {
|
|
129
|
+
ids: nodeId || undefined,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
document: response.data.document,
|
|
134
|
+
components: response.data.components || {},
|
|
135
|
+
styles: response.data.styles || {},
|
|
136
|
+
name: response.data.name,
|
|
137
|
+
lastModified: response.data.lastModified,
|
|
138
|
+
thumbnailUrl: response.data.thumbnailUrl,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (axios.isAxiosError(error)) {
|
|
143
|
+
throw new Error(`Figma API error: ${error.response?.data?.message || error.message}`);
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Figma 파일 구조 분석
|
|
150
|
+
*/
|
|
151
|
+
async analyzeFigmaFile(url) {
|
|
152
|
+
const fileData = await this.getFigmaData(url);
|
|
153
|
+
const analysis = this.analyzeFileStructure(fileData);
|
|
154
|
+
return this.formatAnalysis(analysis);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 파일 구조 분석 및 컴포넌트 정보 추출
|
|
158
|
+
*/
|
|
159
|
+
analyzeFileStructure(file) {
|
|
160
|
+
const stats = this.countNodes(file.document);
|
|
161
|
+
const availableComponents = Object.keys(file.components);
|
|
162
|
+
return {
|
|
163
|
+
totalNodes: stats.total,
|
|
164
|
+
componentCount: stats.components,
|
|
165
|
+
frameCount: stats.frames,
|
|
166
|
+
textCount: stats.text,
|
|
167
|
+
availableComponents,
|
|
168
|
+
suggestedMappings: this.suggestComponentMappings(availableComponents),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 파일에 있는 다른 유형의 노드 수 세기
|
|
173
|
+
*/
|
|
174
|
+
countNodes(node) {
|
|
175
|
+
let total = 1;
|
|
176
|
+
let components = node.type === 'COMPONENT' ? 1 : 0;
|
|
177
|
+
let frames = node.type === 'FRAME' ? 1 : 0;
|
|
178
|
+
let text = node.type === 'TEXT' ? 1 : 0;
|
|
179
|
+
if (node.children) {
|
|
180
|
+
for (const child of node.children) {
|
|
181
|
+
const childStats = this.countNodes(child);
|
|
182
|
+
total += childStats.total;
|
|
183
|
+
components += childStats.components;
|
|
184
|
+
frames += childStats.frames;
|
|
185
|
+
text += childStats.text;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { total, components, frames, text };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Figma 컴포넌트와 디자인 시스템 컴포넌트 간의 매핑 제안
|
|
192
|
+
*/
|
|
193
|
+
suggestComponentMappings(figmaComponents) {
|
|
194
|
+
const mappings = [];
|
|
195
|
+
// 일반적인 컴포넌트 이름 매핑
|
|
196
|
+
const commonMappings = {
|
|
197
|
+
'button': ['Button', 'Btn', 'PrimaryButton', 'SecondaryButton'],
|
|
198
|
+
'input': ['Input', 'TextField', 'TextInput'],
|
|
199
|
+
'card': ['Card', 'Panel', 'Container'],
|
|
200
|
+
'modal': ['Modal', 'Dialog', 'Popup'],
|
|
201
|
+
'header': ['Header', 'Navbar', 'Navigation'],
|
|
202
|
+
'footer': ['Footer', 'BottomBar'],
|
|
203
|
+
'sidebar': ['Sidebar', 'Drawer', 'Navigation'],
|
|
204
|
+
'form': ['Form', 'FormGroup'],
|
|
205
|
+
'table': ['Table', 'DataTable'],
|
|
206
|
+
'list': ['List', 'ListItem'],
|
|
207
|
+
'avatar': ['Avatar', 'ProfileImage'],
|
|
208
|
+
'badge': ['Badge', 'Tag', 'Label'],
|
|
209
|
+
'tooltip': ['Tooltip', 'Popover'],
|
|
210
|
+
'dropdown': ['Dropdown', 'Select'],
|
|
211
|
+
'checkbox': ['Checkbox', 'CheckBox'],
|
|
212
|
+
'radio': ['Radio', 'RadioButton'],
|
|
213
|
+
'switch': ['Switch', 'Toggle'],
|
|
214
|
+
'slider': ['Slider', 'Range'],
|
|
215
|
+
'progress': ['Progress', 'ProgressBar'],
|
|
216
|
+
'spinner': ['Spinner', 'Loader'],
|
|
217
|
+
'alert': ['Alert', 'Notification'],
|
|
218
|
+
'breadcrumb': ['Breadcrumb', 'Breadcrumbs'],
|
|
219
|
+
'pagination': ['Pagination', 'Pager'],
|
|
220
|
+
'tabs': ['Tabs', 'TabList'],
|
|
221
|
+
'accordion': ['Accordion', 'Collapse'],
|
|
222
|
+
'carousel': ['Carousel', 'Slider'],
|
|
223
|
+
'stepper': ['Stepper', 'Steps'],
|
|
224
|
+
};
|
|
225
|
+
for (const figmaComponent of figmaComponents) {
|
|
226
|
+
const lowerName = figmaComponent.toLowerCase();
|
|
227
|
+
for (const [key, possibleNames] of Object.entries(commonMappings)) {
|
|
228
|
+
for (const possibleName of possibleNames) {
|
|
229
|
+
if (lowerName.includes(key) || lowerName.includes(possibleName.toLowerCase())) {
|
|
230
|
+
mappings.push({
|
|
231
|
+
figmaComponent,
|
|
232
|
+
designSystemComponent: possibleName,
|
|
233
|
+
confidence: 0.8,
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return mappings;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 분석 결과 표시 형식 지정
|
|
244
|
+
*/
|
|
245
|
+
formatAnalysis(analysis) {
|
|
246
|
+
let result = `## Figma File Analysis\n\n`;
|
|
247
|
+
result += `**File Statistics:**\n`;
|
|
248
|
+
result += `- Total Nodes: ${analysis.totalNodes}\n`;
|
|
249
|
+
result += `- Components: ${analysis.componentCount}\n`;
|
|
250
|
+
result += `- Frames: ${analysis.frameCount}\n`;
|
|
251
|
+
result += `- Text Elements: ${analysis.textCount}\n\n`;
|
|
252
|
+
if (analysis.availableComponents.length > 0) {
|
|
253
|
+
result += `**Available Components:**\n`;
|
|
254
|
+
analysis.availableComponents.forEach(comp => {
|
|
255
|
+
result += `- ${comp}\n`;
|
|
256
|
+
});
|
|
257
|
+
result += `\n`;
|
|
258
|
+
}
|
|
259
|
+
if (analysis.suggestedMappings.length > 0) {
|
|
260
|
+
result += `**Suggested Component Mappings:**\n`;
|
|
261
|
+
analysis.suggestedMappings.forEach(mapping => {
|
|
262
|
+
result += `- ${mapping.figmaComponent} → ${mapping.designSystemComponent} (${Math.round(mapping.confidence * 100)}% confidence)\n`;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Figma 파일에서 디자인 토큰 추출
|
|
269
|
+
*/
|
|
270
|
+
extractDesignTokens(file) {
|
|
271
|
+
const tokens = {
|
|
272
|
+
colors: {},
|
|
273
|
+
typography: {},
|
|
274
|
+
spacing: {},
|
|
275
|
+
borderRadius: {},
|
|
276
|
+
};
|
|
277
|
+
// 스타일에서 색상 추출
|
|
278
|
+
for (const [key, style] of Object.entries(file.styles)) {
|
|
279
|
+
if (style.styleType === 'FILL') {
|
|
280
|
+
tokens.colors[key] = style.description || key;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// 타이포그래피 추출
|
|
284
|
+
for (const [key, style] of Object.entries(file.styles)) {
|
|
285
|
+
if (style.styleType === 'TEXT') {
|
|
286
|
+
tokens.typography[key] = {
|
|
287
|
+
fontFamily: style.fontFamily,
|
|
288
|
+
fontSize: style.fontSize,
|
|
289
|
+
fontWeight: style.fontWeight,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return tokens;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma Desktop MCP 서버와 통신하는 클라이언트
|
|
3
|
+
* HTTP 기반 MCP 서버와 JSON-RPC 2.0 프로토콜로 통신
|
|
4
|
+
*/
|
|
5
|
+
export declare class FigmaMCPClient {
|
|
6
|
+
private client;
|
|
7
|
+
private baseUrl;
|
|
8
|
+
private requestId;
|
|
9
|
+
constructor(baseUrl?: string);
|
|
10
|
+
/**
|
|
11
|
+
* JSON-RPC 2.0 요청 생성
|
|
12
|
+
*/
|
|
13
|
+
private createRequest;
|
|
14
|
+
/**
|
|
15
|
+
* MCP 서버에 요청 전송
|
|
16
|
+
* HTTP 기반 MCP 서버는 JSON-RPC 2.0 프로토콜을 사용
|
|
17
|
+
*/
|
|
18
|
+
private sendRequest;
|
|
19
|
+
/**
|
|
20
|
+
* 사용 가능한 도구 목록 가져오기
|
|
21
|
+
*/
|
|
22
|
+
listTools(): Promise<any[]>;
|
|
23
|
+
/**
|
|
24
|
+
* MCP 도구 호출
|
|
25
|
+
*/
|
|
26
|
+
private callTool;
|
|
27
|
+
/**
|
|
28
|
+
* Figma 파일 데이터 가져오기
|
|
29
|
+
* Figma MCP 서버의 도구를 사용하여 파일 정보 가져오기
|
|
30
|
+
*/
|
|
31
|
+
getFileData(fileId: string, nodeId?: string): Promise<any>;
|
|
32
|
+
/**
|
|
33
|
+
* Figma 노드 정보 가져오기
|
|
34
|
+
*/
|
|
35
|
+
getNodeData(fileId: string, nodeId: string): Promise<any>;
|
|
36
|
+
/**
|
|
37
|
+
* MCP 서버 연결 상태 확인
|
|
38
|
+
*/
|
|
39
|
+
isAvailable(): Promise<boolean>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
/**
|
|
3
|
+
* Figma Desktop MCP 서버와 통신하는 클라이언트
|
|
4
|
+
* HTTP 기반 MCP 서버와 JSON-RPC 2.0 프로토콜로 통신
|
|
5
|
+
*/
|
|
6
|
+
export class FigmaMCPClient {
|
|
7
|
+
client;
|
|
8
|
+
baseUrl;
|
|
9
|
+
requestId = 0;
|
|
10
|
+
constructor(baseUrl = 'http://127.0.0.1:3845/mcp') {
|
|
11
|
+
this.baseUrl = baseUrl;
|
|
12
|
+
this.client = axios.create({
|
|
13
|
+
baseURL: baseUrl,
|
|
14
|
+
timeout: 30000,
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* JSON-RPC 2.0 요청 생성
|
|
22
|
+
*/
|
|
23
|
+
createRequest(method, params) {
|
|
24
|
+
return {
|
|
25
|
+
jsonrpc: '2.0',
|
|
26
|
+
id: ++this.requestId,
|
|
27
|
+
method,
|
|
28
|
+
params: params || {},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* MCP 서버에 요청 전송
|
|
33
|
+
* HTTP 기반 MCP 서버는 JSON-RPC 2.0 프로토콜을 사용
|
|
34
|
+
*/
|
|
35
|
+
async sendRequest(method, params) {
|
|
36
|
+
try {
|
|
37
|
+
const request = this.createRequest(method, params);
|
|
38
|
+
// HTTP 기반 MCP 서버에 POST 요청
|
|
39
|
+
const response = await this.client.post('', request);
|
|
40
|
+
if (response.data.error) {
|
|
41
|
+
throw new Error(`MCP Error: ${response.data.error.message || 'Unknown error'}`);
|
|
42
|
+
}
|
|
43
|
+
return response.data.result;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (axios.isAxiosError(error)) {
|
|
47
|
+
// 연결 실패 시 null 반환 (폴백을 위해)
|
|
48
|
+
if (error.code === 'ECONNREFUSED' ||
|
|
49
|
+
error.code === 'ETIMEDOUT' ||
|
|
50
|
+
error.response?.status === 404) {
|
|
51
|
+
console.warn(`Figma MCP 서버 연결 실패 (${error.code || error.response?.status}), REST API로 폴백합니다.`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Figma MCP connection error: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 사용 가능한 도구 목록 가져오기
|
|
61
|
+
*/
|
|
62
|
+
async listTools() {
|
|
63
|
+
const result = await this.sendRequest('tools/list');
|
|
64
|
+
return result?.tools || [];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* MCP 도구 호출
|
|
68
|
+
*/
|
|
69
|
+
async callTool(toolName, args) {
|
|
70
|
+
try {
|
|
71
|
+
const result = await this.sendRequest('tools/call', {
|
|
72
|
+
name: toolName,
|
|
73
|
+
arguments: args,
|
|
74
|
+
});
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.warn(`Figma MCP 도구 ${toolName} 호출 실패:`, error);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Figma 파일 데이터 가져오기
|
|
84
|
+
* Figma MCP 서버의 도구를 사용하여 파일 정보 가져오기
|
|
85
|
+
*/
|
|
86
|
+
async getFileData(fileId, nodeId) {
|
|
87
|
+
try {
|
|
88
|
+
// 먼저 도구 목록을 확인
|
|
89
|
+
const tools = await this.listTools();
|
|
90
|
+
if (!tools || tools.length === 0) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
// 파일 데이터를 가져오는 도구 찾기
|
|
94
|
+
// Figma Desktop MCP 서버가 제공하는 일반적인 도구 이름들 시도
|
|
95
|
+
const possibleToolNames = [
|
|
96
|
+
'get_file',
|
|
97
|
+
'getFile',
|
|
98
|
+
'fetch_file',
|
|
99
|
+
'fetchFile',
|
|
100
|
+
'get_figma_file',
|
|
101
|
+
'figma_get_file',
|
|
102
|
+
];
|
|
103
|
+
for (const toolName of possibleToolNames) {
|
|
104
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
105
|
+
if (tool) {
|
|
106
|
+
const params = { fileId };
|
|
107
|
+
if (nodeId) {
|
|
108
|
+
params.nodeId = nodeId;
|
|
109
|
+
}
|
|
110
|
+
const result = await this.callTool(toolName, params);
|
|
111
|
+
if (result) {
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 도구를 찾지 못한 경우 null 반환 (폴백 사용)
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.warn('Figma MCP 파일 데이터 가져오기 실패:', error);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Figma 노드 정보 가져오기
|
|
126
|
+
*/
|
|
127
|
+
async getNodeData(fileId, nodeId) {
|
|
128
|
+
try {
|
|
129
|
+
const tools = await this.listTools();
|
|
130
|
+
if (!tools || tools.length === 0) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// 노드 데이터를 가져오는 도구 찾기
|
|
134
|
+
const possibleToolNames = [
|
|
135
|
+
'get_node',
|
|
136
|
+
'getNode',
|
|
137
|
+
'fetch_node',
|
|
138
|
+
'fetchNode',
|
|
139
|
+
'get_figma_node',
|
|
140
|
+
'figma_get_node',
|
|
141
|
+
];
|
|
142
|
+
for (const toolName of possibleToolNames) {
|
|
143
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
144
|
+
if (tool) {
|
|
145
|
+
const result = await this.callTool(toolName, { fileId, nodeId });
|
|
146
|
+
if (result) {
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 노드 전용 도구가 없으면 파일 도구에 nodeId를 전달하여 시도
|
|
152
|
+
return await this.getFileData(fileId, nodeId);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.warn('Figma MCP 노드 데이터 가져오기 실패:', error);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* MCP 서버 연결 상태 확인
|
|
161
|
+
*/
|
|
162
|
+
async isAvailable() {
|
|
163
|
+
try {
|
|
164
|
+
await this.listTools();
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 요청 ID 생성 (타임스탬프 + 랜덤 문자열)
|
|
3
|
+
*/
|
|
4
|
+
export declare function generateRequestId(): string;
|
|
5
|
+
/**
|
|
6
|
+
* 요청별 폴더 경로 생성
|
|
7
|
+
*/
|
|
8
|
+
export declare function getRequestFolderPath(requestId: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* 요청 폴더 생성
|
|
11
|
+
*/
|
|
12
|
+
export declare function createRequestFolder(requestId: string): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* 파일 저장
|
|
15
|
+
*/
|
|
16
|
+
export declare function saveFile(requestId: string, filename: string, content: string): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* 바이너리 파일 저장 (이미지 등)
|
|
19
|
+
*/
|
|
20
|
+
export declare function saveBinaryFile(requestId: string, filename: string, content: Buffer): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* 요청 메타데이터 저장
|
|
23
|
+
*/
|
|
24
|
+
export interface RequestMetadata {
|
|
25
|
+
requestId: string;
|
|
26
|
+
type: 'react' | 'vue';
|
|
27
|
+
componentName: string;
|
|
28
|
+
figmaUrl: string;
|
|
29
|
+
nodeId?: string;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
files: Array<{
|
|
32
|
+
name: string;
|
|
33
|
+
path: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare function saveMetadata(requestId: string, metadata: Omit<RequestMetadata, 'requestId' | 'createdAt' | 'files'>): Promise<string>;
|