palette-mcp 1.1.2 → 1.2.1
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/server.d.ts +15 -0
- package/dist/server.js +214 -21
- package/dist/services/auth.d.ts +22 -0
- package/dist/services/auth.js +138 -0
- package/dist/services/code-generator.js +20 -7
- package/dist/services/design-system.d.ts +43 -1
- package/dist/services/design-system.js +84 -1
- package/dist/smithery.d.ts +57 -32
- package/dist/smithery.js +59 -23
- package/dist/utils/request-manager.js +12 -3
- package/package.json +15 -7
- package/smithery.yaml +2 -9
- package/src/server.ts +261 -26
- package/src/services/auth.ts +162 -0
- package/src/services/code-generator.ts +21 -6
- package/src/services/design-system.ts +106 -1
- package/src/smithery.ts +63 -23
- package/src/utils/request-manager.ts +14 -7
package/dist/server.d.ts
CHANGED
|
@@ -5,10 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
7
|
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { type SyncConfig } from './services/design-system.js';
|
|
8
9
|
export interface ServerConfig {
|
|
9
10
|
figmaAccessToken?: string;
|
|
10
11
|
githubToken?: string;
|
|
11
12
|
figmaMcpServerUrl?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Figma Desktop MCP 클라이언트 사용 여부
|
|
15
|
+
* Remote 모드(Smithery)에서는 false로 설정해야 함 (로컬호스트 접근 불가)
|
|
16
|
+
*/
|
|
17
|
+
useFigmaMcp?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* 디자인 시스템 컴포넌트 동기화 설정
|
|
20
|
+
*/
|
|
21
|
+
syncConfig?: SyncConfig;
|
|
22
|
+
/**
|
|
23
|
+
* 인증 건너뛰기 (로컬 개발용)
|
|
24
|
+
* true로 설정하면 GitHub 조직 멤버십 확인을 건너뜁니다.
|
|
25
|
+
*/
|
|
26
|
+
skipAuth?: boolean;
|
|
12
27
|
}
|
|
13
28
|
export declare const tools: Tool[];
|
|
14
29
|
/**
|
package/dist/server.js
CHANGED
|
@@ -4,35 +4,43 @@
|
|
|
4
4
|
* Local(stdio) 모드와 Remote(Smithery) 모드에서 공유되는 핵심 기능을 정의합니다.
|
|
5
5
|
*/
|
|
6
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { readFile } from 'fs/promises';
|
|
9
9
|
import { FigmaService } from './services/figma.js';
|
|
10
10
|
import { DesignSystemService } from './services/design-system.js';
|
|
11
11
|
import { CodeGenerator } from './services/code-generator.js';
|
|
12
|
-
|
|
12
|
+
import { validateAccess } from './services/auth.js';
|
|
13
|
+
// Tools 정의 (annotations 포함)
|
|
13
14
|
export const tools = [
|
|
14
15
|
{
|
|
15
16
|
name: 'convert_figma_to_react',
|
|
16
|
-
description: 'Figma 디자인을
|
|
17
|
+
description: 'Figma 디자인을 @dealicious/design-system-react 컴포넌트를 사용하여 React 컴포넌트로 변환합니다. Figma 파일 URL과 컴포넌트 이름을 제공하면, 디자인 시스템 컴포넌트(Button, Text, Tag, Check, Chip 등)를 활용한 React TSX 코드를 생성합니다.',
|
|
18
|
+
annotations: {
|
|
19
|
+
title: 'Figma to React Converter',
|
|
20
|
+
readOnlyHint: false,
|
|
21
|
+
destructiveHint: false,
|
|
22
|
+
idempotentHint: true,
|
|
23
|
+
openWorldHint: true,
|
|
24
|
+
},
|
|
17
25
|
inputSchema: {
|
|
18
26
|
type: 'object',
|
|
19
27
|
properties: {
|
|
20
28
|
figmaUrl: {
|
|
21
29
|
type: 'string',
|
|
22
|
-
description: 'Figma
|
|
30
|
+
description: 'Figma 파일의 전체 URL (예: https://www.figma.com/file/ABC123/Design?node-id=1-2) 또는 파일 ID',
|
|
23
31
|
},
|
|
24
32
|
nodeId: {
|
|
25
33
|
type: 'string',
|
|
26
|
-
description: '변환할 특정
|
|
34
|
+
description: '변환할 특정 Figma 노드의 ID. URL에 node-id가 포함되어 있으면 생략 가능합니다.',
|
|
27
35
|
},
|
|
28
36
|
componentName: {
|
|
29
37
|
type: 'string',
|
|
30
|
-
description: '
|
|
38
|
+
description: '생성될 React 컴포넌트의 이름 (예: ProductCard, LoginForm)',
|
|
31
39
|
},
|
|
32
40
|
previewType: {
|
|
33
41
|
type: 'string',
|
|
34
42
|
enum: ['html', 'image', 'both'],
|
|
35
|
-
description: '미리보기
|
|
43
|
+
description: '미리보기 생성 옵션: "html"은 브라우저에서 볼 수 있는 HTML 파일, "image"는 PNG 스크린샷, "both"는 둘 다 생성 (기본값: "both")',
|
|
36
44
|
default: 'both',
|
|
37
45
|
},
|
|
38
46
|
},
|
|
@@ -41,26 +49,33 @@ export const tools = [
|
|
|
41
49
|
},
|
|
42
50
|
{
|
|
43
51
|
name: 'convert_figma_to_vue',
|
|
44
|
-
description: 'Figma 디자인을
|
|
52
|
+
description: 'Figma 디자인을 @dealicious/design-system Vue 컴포넌트를 사용하여 Vue 3 Single File Component로 변환합니다. Figma 파일 URL과 컴포넌트 이름을 제공하면, 디자인 시스템 컴포넌트를 활용한 Vue SFC 코드를 생성합니다.',
|
|
53
|
+
annotations: {
|
|
54
|
+
title: 'Figma to Vue Converter',
|
|
55
|
+
readOnlyHint: false,
|
|
56
|
+
destructiveHint: false,
|
|
57
|
+
idempotentHint: true,
|
|
58
|
+
openWorldHint: true,
|
|
59
|
+
},
|
|
45
60
|
inputSchema: {
|
|
46
61
|
type: 'object',
|
|
47
62
|
properties: {
|
|
48
63
|
figmaUrl: {
|
|
49
64
|
type: 'string',
|
|
50
|
-
description: 'Figma
|
|
65
|
+
description: 'Figma 파일의 전체 URL (예: https://www.figma.com/file/ABC123/Design?node-id=1-2) 또는 파일 ID',
|
|
51
66
|
},
|
|
52
67
|
nodeId: {
|
|
53
68
|
type: 'string',
|
|
54
|
-
description: '변환할 특정
|
|
69
|
+
description: '변환할 특정 Figma 노드의 ID. URL에 node-id가 포함되어 있으면 생략 가능합니다.',
|
|
55
70
|
},
|
|
56
71
|
componentName: {
|
|
57
72
|
type: 'string',
|
|
58
|
-
description: '
|
|
73
|
+
description: '생성될 Vue 컴포넌트의 이름 (예: ProductCard, LoginForm)',
|
|
59
74
|
},
|
|
60
75
|
previewType: {
|
|
61
76
|
type: 'string',
|
|
62
77
|
enum: ['html', 'image', 'both'],
|
|
63
|
-
description: '미리보기
|
|
78
|
+
description: '미리보기 생성 옵션: "html"은 브라우저에서 볼 수 있는 HTML 파일, "image"는 PNG 스크린샷, "both"는 둘 다 생성 (기본값: "both")',
|
|
64
79
|
default: 'both',
|
|
65
80
|
},
|
|
66
81
|
},
|
|
@@ -69,14 +84,21 @@ export const tools = [
|
|
|
69
84
|
},
|
|
70
85
|
{
|
|
71
86
|
name: 'list_design_system_components',
|
|
72
|
-
description: '
|
|
87
|
+
description: '@dealicious/design-system에서 사용 가능한 모든 UI 컴포넌트 목록을 조회합니다. 각 컴포넌트의 이름, 설명, import 경로, 사용 가능한 props를 반환합니다.',
|
|
88
|
+
annotations: {
|
|
89
|
+
title: 'Design System Component List',
|
|
90
|
+
readOnlyHint: true,
|
|
91
|
+
destructiveHint: false,
|
|
92
|
+
idempotentHint: true,
|
|
93
|
+
openWorldHint: false,
|
|
94
|
+
},
|
|
73
95
|
inputSchema: {
|
|
74
96
|
type: 'object',
|
|
75
97
|
properties: {
|
|
76
98
|
framework: {
|
|
77
99
|
type: 'string',
|
|
78
100
|
enum: ['react', 'vue'],
|
|
79
|
-
description: '컴포넌트를 조회할
|
|
101
|
+
description: '컴포넌트를 조회할 프레임워크: "react"는 design-system-react, "vue"는 design-system 패키지',
|
|
80
102
|
},
|
|
81
103
|
},
|
|
82
104
|
required: ['framework'],
|
|
@@ -84,19 +106,76 @@ export const tools = [
|
|
|
84
106
|
},
|
|
85
107
|
{
|
|
86
108
|
name: 'analyze_figma_file',
|
|
87
|
-
description: 'Figma 파일
|
|
109
|
+
description: 'Figma 파일의 구조를 분석하여 페이지, 프레임, 컴포넌트 계층을 파악합니다. 변환하기 전에 파일 구조를 이해하는 데 유용합니다.',
|
|
110
|
+
annotations: {
|
|
111
|
+
title: 'Figma File Analyzer',
|
|
112
|
+
readOnlyHint: true,
|
|
113
|
+
destructiveHint: false,
|
|
114
|
+
idempotentHint: true,
|
|
115
|
+
openWorldHint: true,
|
|
116
|
+
},
|
|
88
117
|
inputSchema: {
|
|
89
118
|
type: 'object',
|
|
90
119
|
properties: {
|
|
91
120
|
figmaUrl: {
|
|
92
121
|
type: 'string',
|
|
93
|
-
description: 'Figma
|
|
122
|
+
description: '분석할 Figma 파일의 전체 URL 또는 파일 ID',
|
|
94
123
|
},
|
|
95
124
|
},
|
|
96
125
|
required: ['figmaUrl'],
|
|
97
126
|
},
|
|
98
127
|
},
|
|
99
128
|
];
|
|
129
|
+
// Prompts 정의
|
|
130
|
+
const prompts = [
|
|
131
|
+
{
|
|
132
|
+
name: 'convert-design-to-component',
|
|
133
|
+
description: 'Figma 디자인을 React 또는 Vue 컴포넌트로 변환하는 가이드 프롬프트',
|
|
134
|
+
arguments: [
|
|
135
|
+
{
|
|
136
|
+
name: 'figmaUrl',
|
|
137
|
+
description: 'Figma 파일 URL',
|
|
138
|
+
required: true,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'framework',
|
|
142
|
+
description: '프레임워크 선택 (react 또는 vue)',
|
|
143
|
+
required: true,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'componentName',
|
|
147
|
+
description: '컴포넌트 이름',
|
|
148
|
+
required: true,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'explore-design-system',
|
|
154
|
+
description: '디자인 시스템 컴포넌트 탐색 가이드',
|
|
155
|
+
arguments: [
|
|
156
|
+
{
|
|
157
|
+
name: 'framework',
|
|
158
|
+
description: '프레임워크 선택 (react 또는 vue)',
|
|
159
|
+
required: false,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
// Resources 정의
|
|
165
|
+
const resources = [
|
|
166
|
+
{
|
|
167
|
+
uri: 'palette://design-system/react/components',
|
|
168
|
+
name: 'React Design System Components',
|
|
169
|
+
description: '@dealicious/design-system-react에서 사용 가능한 컴포넌트 목록',
|
|
170
|
+
mimeType: 'application/json',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
uri: 'palette://design-system/vue/components',
|
|
174
|
+
name: 'Vue Design System Components',
|
|
175
|
+
description: '@dealicious/design-system에서 사용 가능한 컴포넌트 목록',
|
|
176
|
+
mimeType: 'application/json',
|
|
177
|
+
},
|
|
178
|
+
];
|
|
100
179
|
/**
|
|
101
180
|
* MCP 서버를 생성하고 핸들러를 등록합니다.
|
|
102
181
|
* Local 모드와 Remote 모드에서 공통으로 사용됩니다.
|
|
@@ -112,23 +191,59 @@ export function createPaletteServer(config = {}) {
|
|
|
112
191
|
if (config.figmaMcpServerUrl) {
|
|
113
192
|
process.env.FIGMA_MCP_SERVER_URL = config.figmaMcpServerUrl;
|
|
114
193
|
}
|
|
115
|
-
// MCP 서버 초기화
|
|
194
|
+
// MCP 서버 초기화 (모든 capabilities 활성화)
|
|
116
195
|
const server = new Server({
|
|
117
196
|
name: 'palette',
|
|
118
197
|
version: '1.0.0',
|
|
198
|
+
}, {
|
|
199
|
+
capabilities: {
|
|
200
|
+
tools: {}, // tools 기능 활성화
|
|
201
|
+
prompts: {}, // prompts 기능 활성화
|
|
202
|
+
resources: {}, // resources 기능 활성화
|
|
203
|
+
},
|
|
119
204
|
});
|
|
120
205
|
// 서비스 초기화
|
|
121
|
-
|
|
122
|
-
const
|
|
206
|
+
// Remote 모드에서는 Figma Desktop MCP 클라이언트를 사용하지 않음 (로컬호스트 접근 불가)
|
|
207
|
+
const useFigmaMcp = config.useFigmaMcp !== undefined ? config.useFigmaMcp : true;
|
|
208
|
+
const figmaService = new FigmaService(useFigmaMcp, config.figmaMcpServerUrl);
|
|
209
|
+
const designSystemService = new DesignSystemService(config.syncConfig);
|
|
123
210
|
const codeGenerator = new CodeGenerator(designSystemService);
|
|
124
|
-
//
|
|
211
|
+
// 인증 상태 캐시
|
|
212
|
+
let authResult = null;
|
|
213
|
+
/**
|
|
214
|
+
* 도구 실행 전 인증 확인
|
|
215
|
+
* skipAuth가 true이면 인증을 건너뜁니다.
|
|
216
|
+
*/
|
|
217
|
+
async function ensureAuthenticated() {
|
|
218
|
+
// 로컬 개발 모드에서는 인증 건너뛰기
|
|
219
|
+
if (config.skipAuth) {
|
|
220
|
+
console.error('[Palette Auth] ⚠️ 인증 건너뛰기 모드 (skipAuth=true)');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// 이미 인증되었으면 건너뛰기
|
|
224
|
+
if (authResult?.authorized) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// 인증 수행
|
|
228
|
+
const githubToken = config.githubToken || process.env.GITHUB_TOKEN;
|
|
229
|
+
authResult = await validateAccess(githubToken);
|
|
230
|
+
if (!authResult.authorized) {
|
|
231
|
+
throw new Error(authResult.error || '인증에 실패했습니다.');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// 디자인 시스템 컴포넌트 비동기 초기화 (백그라운드에서 실행)
|
|
235
|
+
designSystemService.initialize().catch((error) => {
|
|
236
|
+
console.error('[Palette] 디자인 시스템 초기화 실패:', error);
|
|
237
|
+
});
|
|
238
|
+
// ===== Tools 핸들러 =====
|
|
125
239
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
126
240
|
return { tools };
|
|
127
241
|
});
|
|
128
|
-
// 도구 실행 핸들러
|
|
129
242
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
130
243
|
const { name, arguments: args } = request.params;
|
|
131
244
|
try {
|
|
245
|
+
// 🔐 도구 실행 전 인증 확인
|
|
246
|
+
await ensureAuthenticated();
|
|
132
247
|
switch (name) {
|
|
133
248
|
case 'convert_figma_to_react': {
|
|
134
249
|
const { figmaUrl, nodeId, componentName, previewType = 'both' } = args;
|
|
@@ -242,5 +357,83 @@ export function createPaletteServer(config = {}) {
|
|
|
242
357
|
};
|
|
243
358
|
}
|
|
244
359
|
});
|
|
360
|
+
// ===== Prompts 핸들러 =====
|
|
361
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
362
|
+
return { prompts };
|
|
363
|
+
});
|
|
364
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
365
|
+
const { name, arguments: args } = request.params;
|
|
366
|
+
switch (name) {
|
|
367
|
+
case 'convert-design-to-component': {
|
|
368
|
+
const figmaUrl = args?.figmaUrl || '<FIGMA_URL>';
|
|
369
|
+
const framework = args?.framework || 'react';
|
|
370
|
+
const componentName = args?.componentName || 'MyComponent';
|
|
371
|
+
return {
|
|
372
|
+
description: 'Figma 디자인을 컴포넌트로 변환하는 프롬프트',
|
|
373
|
+
messages: [
|
|
374
|
+
{
|
|
375
|
+
role: 'user',
|
|
376
|
+
content: {
|
|
377
|
+
type: 'text',
|
|
378
|
+
text: `Figma 디자인을 ${framework} 컴포넌트로 변환해주세요.\n\n- Figma URL: ${figmaUrl}\n- 컴포넌트 이름: ${componentName}\n\n디자인 시스템 컴포넌트를 최대한 활용해주세요.`,
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
case 'explore-design-system': {
|
|
385
|
+
const framework = args?.framework || 'react';
|
|
386
|
+
return {
|
|
387
|
+
description: '디자인 시스템 탐색 프롬프트',
|
|
388
|
+
messages: [
|
|
389
|
+
{
|
|
390
|
+
role: 'user',
|
|
391
|
+
content: {
|
|
392
|
+
type: 'text',
|
|
393
|
+
text: `@dealicious/design-system${framework === 'react' ? '-react' : ''} 패키지에서 사용 가능한 컴포넌트 목록을 보여주세요. 각 컴포넌트의 사용법과 예제도 함께 알려주세요.`,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
default:
|
|
400
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
// ===== Resources 핸들러 =====
|
|
404
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
405
|
+
return { resources };
|
|
406
|
+
});
|
|
407
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
408
|
+
const { uri } = request.params;
|
|
409
|
+
switch (uri) {
|
|
410
|
+
case 'palette://design-system/react/components': {
|
|
411
|
+
const components = await designSystemService.getAvailableComponents('react');
|
|
412
|
+
return {
|
|
413
|
+
contents: [
|
|
414
|
+
{
|
|
415
|
+
uri,
|
|
416
|
+
mimeType: 'application/json',
|
|
417
|
+
text: JSON.stringify(components, null, 2),
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
case 'palette://design-system/vue/components': {
|
|
423
|
+
const components = await designSystemService.getAvailableComponents('vue');
|
|
424
|
+
return {
|
|
425
|
+
contents: [
|
|
426
|
+
{
|
|
427
|
+
uri,
|
|
428
|
+
mimeType: 'application/json',
|
|
429
|
+
text: JSON.stringify(components, null, 2),
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
default:
|
|
435
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
245
438
|
return server;
|
|
246
439
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 인증 및 권한 검증 서비스
|
|
3
|
+
*
|
|
4
|
+
* dealicious-inc 조직 멤버십을 확인하여 서비스 접근을 제어합니다.
|
|
5
|
+
*/
|
|
6
|
+
export interface AuthResult {
|
|
7
|
+
authorized: boolean;
|
|
8
|
+
username?: string;
|
|
9
|
+
organization?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* GitHub 조직 멤버십을 확인하여 서비스 접근 권한을 검증합니다.
|
|
14
|
+
*
|
|
15
|
+
* @param githubToken GitHub Personal Access Token
|
|
16
|
+
* @returns 인증 결과
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateAccess(githubToken?: string): Promise<AuthResult>;
|
|
19
|
+
/**
|
|
20
|
+
* 인증 캐시 초기화 (테스트용)
|
|
21
|
+
*/
|
|
22
|
+
export declare function clearAuthCache(): void;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 인증 및 권한 검증 서비스
|
|
3
|
+
*
|
|
4
|
+
* dealicious-inc 조직 멤버십을 확인하여 서비스 접근을 제어합니다.
|
|
5
|
+
*/
|
|
6
|
+
import { Octokit } from '@octokit/rest';
|
|
7
|
+
// 허용된 GitHub 조직 목록
|
|
8
|
+
const ALLOWED_ORGANIZATIONS = ['dealicious-inc'];
|
|
9
|
+
// 인증 상태 캐시 (토큰별로 검증 결과를 캐시)
|
|
10
|
+
const authCache = new Map();
|
|
11
|
+
// 캐시 유효 시간 (1시간)
|
|
12
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* GitHub 토큰으로 사용자 정보 조회
|
|
15
|
+
*/
|
|
16
|
+
async function getAuthenticatedUser(octokit) {
|
|
17
|
+
const { data } = await octokit.users.getAuthenticated();
|
|
18
|
+
return { login: data.login, email: data.email };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 사용자가 허용된 조직의 멤버인지 확인
|
|
22
|
+
*/
|
|
23
|
+
async function checkOrganizationMembership(octokit, org) {
|
|
24
|
+
try {
|
|
25
|
+
await octokit.orgs.getMembershipForAuthenticatedUser({ org });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
// 404: 멤버가 아님, 403: 권한 없음
|
|
30
|
+
if (error.status === 404 || error.status === 403) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 캐시된 인증 결과 확인
|
|
38
|
+
*/
|
|
39
|
+
function getCachedAuth(githubToken) {
|
|
40
|
+
const cached = authCache.get(githubToken);
|
|
41
|
+
if (!cached)
|
|
42
|
+
return null;
|
|
43
|
+
// 캐시 만료 확인
|
|
44
|
+
const now = new Date();
|
|
45
|
+
if (now.getTime() - cached.checkedAt.getTime() > CACHE_TTL_MS) {
|
|
46
|
+
authCache.delete(githubToken);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return { valid: cached.valid, username: cached.username };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 인증 결과 캐시 저장
|
|
53
|
+
*/
|
|
54
|
+
function setCachedAuth(githubToken, valid, username) {
|
|
55
|
+
authCache.set(githubToken, { valid, username, checkedAt: new Date() });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* GitHub 조직 멤버십을 확인하여 서비스 접근 권한을 검증합니다.
|
|
59
|
+
*
|
|
60
|
+
* @param githubToken GitHub Personal Access Token
|
|
61
|
+
* @returns 인증 결과
|
|
62
|
+
*/
|
|
63
|
+
export async function validateAccess(githubToken) {
|
|
64
|
+
// GITHUB_TOKEN이 없으면 거부
|
|
65
|
+
if (!githubToken) {
|
|
66
|
+
return {
|
|
67
|
+
authorized: false,
|
|
68
|
+
error: 'GITHUB_TOKEN이 필요합니다. dealicious-inc 조직 멤버만 Palette MCP를 사용할 수 있습니다.',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// 캐시 확인
|
|
72
|
+
const cached = getCachedAuth(githubToken);
|
|
73
|
+
if (cached) {
|
|
74
|
+
if (cached.valid) {
|
|
75
|
+
return {
|
|
76
|
+
authorized: true,
|
|
77
|
+
username: cached.username,
|
|
78
|
+
organization: ALLOWED_ORGANIZATIONS[0],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
return {
|
|
83
|
+
authorized: false,
|
|
84
|
+
username: cached.username,
|
|
85
|
+
error: `사용자 '${cached.username}'은(는) dealicious-inc 조직 멤버가 아닙니다.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// GitHub API로 검증
|
|
90
|
+
const octokit = new Octokit({ auth: githubToken });
|
|
91
|
+
try {
|
|
92
|
+
// 사용자 정보 조회
|
|
93
|
+
const user = await getAuthenticatedUser(octokit);
|
|
94
|
+
// 조직 멤버십 확인
|
|
95
|
+
for (const org of ALLOWED_ORGANIZATIONS) {
|
|
96
|
+
const isMember = await checkOrganizationMembership(octokit, org);
|
|
97
|
+
if (isMember) {
|
|
98
|
+
// 캐시 저장
|
|
99
|
+
setCachedAuth(githubToken, true, user.login);
|
|
100
|
+
console.error(`[Palette Auth] ✅ 인증 성공: ${user.login} (${org})`);
|
|
101
|
+
return {
|
|
102
|
+
authorized: true,
|
|
103
|
+
username: user.login,
|
|
104
|
+
organization: org,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 어떤 조직에도 속하지 않음
|
|
109
|
+
setCachedAuth(githubToken, false, user.login);
|
|
110
|
+
console.error(`[Palette Auth] ❌ 인증 실패: ${user.login} - 허용된 조직 멤버 아님`);
|
|
111
|
+
return {
|
|
112
|
+
authorized: false,
|
|
113
|
+
username: user.login,
|
|
114
|
+
error: `사용자 '${user.login}'은(는) dealicious-inc 조직 멤버가 아닙니다. 조직 관리자에게 문의하세요.`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// 토큰 유효성 검사 실패
|
|
119
|
+
if (error.status === 401) {
|
|
120
|
+
return {
|
|
121
|
+
authorized: false,
|
|
122
|
+
error: 'GITHUB_TOKEN이 유효하지 않습니다. 토큰을 확인해주세요.',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// 기타 오류
|
|
126
|
+
console.error('[Palette Auth] 인증 중 오류 발생:', error.message);
|
|
127
|
+
return {
|
|
128
|
+
authorized: false,
|
|
129
|
+
error: `인증 중 오류가 발생했습니다: ${error.message}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 인증 캐시 초기화 (테스트용)
|
|
135
|
+
*/
|
|
136
|
+
export function clearAuthCache() {
|
|
137
|
+
authCache.clear();
|
|
138
|
+
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { saveFile, saveBinaryFile, saveMetadata, generateRequestId, getRequestFolderPath } from '../utils/request-manager.js';
|
|
2
2
|
// puppeteer는 optional dependency로, 없으면 이미지 프리뷰 기능이 비활성화됨
|
|
3
3
|
let puppeteer = null;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
let puppeteerLoaded = false;
|
|
5
|
+
/**
|
|
6
|
+
* puppeteer를 lazy loading으로 로드
|
|
7
|
+
*/
|
|
8
|
+
async function loadPuppeteer() {
|
|
9
|
+
if (puppeteerLoaded)
|
|
10
|
+
return puppeteer;
|
|
11
|
+
try {
|
|
12
|
+
puppeteer = (await import('puppeteer')).default;
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
console.warn('puppeteer를 로드할 수 없습니다. 이미지 프리뷰 기능이 비활성화됩니다.');
|
|
16
|
+
puppeteer = null;
|
|
17
|
+
}
|
|
18
|
+
puppeteerLoaded = true;
|
|
19
|
+
return puppeteer;
|
|
9
20
|
}
|
|
10
21
|
export class CodeGenerator {
|
|
11
22
|
designSystemService;
|
|
@@ -54,12 +65,14 @@ export class CodeGenerator {
|
|
|
54
65
|
* puppeteer가 없으면 빈 문자열 반환
|
|
55
66
|
*/
|
|
56
67
|
async generateImagePreview(htmlContent, componentName, requestId) {
|
|
68
|
+
// puppeteer를 lazy loading으로 로드
|
|
69
|
+
const pptr = await loadPuppeteer();
|
|
57
70
|
// puppeteer가 없으면 이미지 생성 불가
|
|
58
|
-
if (!
|
|
71
|
+
if (!pptr) {
|
|
59
72
|
console.warn('puppeteer가 설치되지 않아 이미지 프리뷰를 생성할 수 없습니다.');
|
|
60
73
|
return '';
|
|
61
74
|
}
|
|
62
|
-
const browser = await
|
|
75
|
+
const browser = await pptr.launch({
|
|
63
76
|
headless: true,
|
|
64
77
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
65
78
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SyncResult } from '../sync/index.js';
|
|
1
2
|
export interface DesignSystemComponent {
|
|
2
3
|
name: string;
|
|
3
4
|
description: string;
|
|
@@ -19,10 +20,51 @@ export interface ComponentExample {
|
|
|
19
20
|
code: string;
|
|
20
21
|
description: string;
|
|
21
22
|
}
|
|
23
|
+
export interface SyncConfig {
|
|
24
|
+
/** 서버 시작 시 자동 동기화 여부 (기본값: true) */
|
|
25
|
+
autoSync?: boolean;
|
|
26
|
+
/** 동기화 상세 로그 출력 여부 */
|
|
27
|
+
verbose?: boolean;
|
|
28
|
+
}
|
|
22
29
|
export declare class DesignSystemService {
|
|
23
30
|
private reactComponents;
|
|
24
31
|
private vueComponents;
|
|
25
|
-
|
|
32
|
+
private syncService;
|
|
33
|
+
private syncConfig;
|
|
34
|
+
private initialized;
|
|
35
|
+
private lastSyncResult;
|
|
36
|
+
constructor(syncConfig?: SyncConfig);
|
|
37
|
+
/**
|
|
38
|
+
* 서비스 초기화 (비동기)
|
|
39
|
+
* 서버 시작 시 호출하여 GitHub에서 최신 컴포넌트 동기화
|
|
40
|
+
*/
|
|
41
|
+
initialize(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* 수동 동기화 실행
|
|
44
|
+
*/
|
|
45
|
+
syncComponents(force?: boolean): Promise<SyncResult>;
|
|
46
|
+
/**
|
|
47
|
+
* 마지막 동기화 결과 조회
|
|
48
|
+
*/
|
|
49
|
+
getLastSyncResult(): SyncResult | null;
|
|
50
|
+
/**
|
|
51
|
+
* 캐시 상태 조회
|
|
52
|
+
*/
|
|
53
|
+
getCacheStatus(): Promise<{
|
|
54
|
+
exists: boolean;
|
|
55
|
+
isValid: boolean;
|
|
56
|
+
metadata: {
|
|
57
|
+
version: string;
|
|
58
|
+
lastSyncedAt: string;
|
|
59
|
+
commitSha: string;
|
|
60
|
+
reactComponentCount: number;
|
|
61
|
+
vueComponentCount: number;
|
|
62
|
+
} | null;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* 캐시 삭제
|
|
66
|
+
*/
|
|
67
|
+
clearCache(): Promise<void>;
|
|
26
68
|
/**
|
|
27
69
|
* 디자인 시스템(React, Vue) 컴포넌트 메타데이터 제공 필요.
|
|
28
70
|
* 1. 메타데이터 구조 정의
|