Skip to main content
Glama
server.ts16.2 kB
/** * Palette MCP Server - 공통 로직 * * Local(stdio) 모드와 Remote(Smithery) 모드에서 공유되는 핵심 기능을 정의합니다. */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { readFile } from 'fs/promises'; import { FigmaService } from './services/figma.js'; import { DesignSystemService } from './services/design-system.js'; import { CodeGenerator, type PreviewType } from './services/code-generator.js'; // 서버 설정 타입 export interface ServerConfig { figmaAccessToken?: string; githubToken?: string; figmaMcpServerUrl?: string; /** * Figma Desktop MCP 클라이언트 사용 여부 * Remote 모드(Smithery)에서는 false로 설정해야 함 (로컬호스트 접근 불가) */ useFigmaMcp?: boolean; } // Tools 정의 (annotations 포함) export const tools: Tool[] = [ { name: 'convert_figma_to_react', description: 'Figma 디자인을 @dealicious/design-system-react 컴포넌트를 사용하여 React 컴포넌트로 변환합니다. Figma 파일 URL과 컴포넌트 이름을 제공하면, 디자인 시스템 컴포넌트(Button, Text, Tag, Check, Chip 등)를 활용한 React TSX 코드를 생성합니다.', annotations: { title: 'Figma to React Converter', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, inputSchema: { type: 'object', properties: { figmaUrl: { type: 'string', description: 'Figma 파일의 전체 URL (예: https://www.figma.com/file/ABC123/Design?node-id=1-2) 또는 파일 ID', }, nodeId: { type: 'string', description: '변환할 특정 Figma 노드의 ID. URL에 node-id가 포함되어 있으면 생략 가능합니다.', }, componentName: { type: 'string', description: '생성될 React 컴포넌트의 이름 (예: ProductCard, LoginForm)', }, previewType: { type: 'string', enum: ['html', 'image', 'both'], description: '미리보기 생성 옵션: "html"은 브라우저에서 볼 수 있는 HTML 파일, "image"는 PNG 스크린샷, "both"는 둘 다 생성 (기본값: "both")', default: 'both', }, }, required: ['figmaUrl', 'componentName'], }, }, { name: 'convert_figma_to_vue', description: 'Figma 디자인을 @dealicious/design-system Vue 컴포넌트를 사용하여 Vue 3 Single File Component로 변환합니다. Figma 파일 URL과 컴포넌트 이름을 제공하면, 디자인 시스템 컴포넌트를 활용한 Vue SFC 코드를 생성합니다.', annotations: { title: 'Figma to Vue Converter', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, inputSchema: { type: 'object', properties: { figmaUrl: { type: 'string', description: 'Figma 파일의 전체 URL (예: https://www.figma.com/file/ABC123/Design?node-id=1-2) 또는 파일 ID', }, nodeId: { type: 'string', description: '변환할 특정 Figma 노드의 ID. URL에 node-id가 포함되어 있으면 생략 가능합니다.', }, componentName: { type: 'string', description: '생성될 Vue 컴포넌트의 이름 (예: ProductCard, LoginForm)', }, previewType: { type: 'string', enum: ['html', 'image', 'both'], description: '미리보기 생성 옵션: "html"은 브라우저에서 볼 수 있는 HTML 파일, "image"는 PNG 스크린샷, "both"는 둘 다 생성 (기본값: "both")', default: 'both', }, }, required: ['figmaUrl', 'componentName'], }, }, { name: 'list_design_system_components', description: '@dealicious/design-system에서 사용 가능한 모든 UI 컴포넌트 목록을 조회합니다. 각 컴포넌트의 이름, 설명, import 경로, 사용 가능한 props를 반환합니다.', annotations: { title: 'Design System Component List', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, inputSchema: { type: 'object', properties: { framework: { type: 'string', enum: ['react', 'vue'], description: '컴포넌트를 조회할 프레임워크: "react"는 design-system-react, "vue"는 design-system 패키지', }, }, required: ['framework'], }, }, { name: 'analyze_figma_file', description: 'Figma 파일의 구조를 분석하여 페이지, 프레임, 컴포넌트 계층을 파악합니다. 변환하기 전에 파일 구조를 이해하는 데 유용합니다.', annotations: { title: 'Figma File Analyzer', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, inputSchema: { type: 'object', properties: { figmaUrl: { type: 'string', description: '분석할 Figma 파일의 전체 URL 또는 파일 ID', }, }, required: ['figmaUrl'], }, }, ]; // Prompts 정의 const prompts = [ { name: 'convert-design-to-component', description: 'Figma 디자인을 React 또는 Vue 컴포넌트로 변환하는 가이드 프롬프트', arguments: [ { name: 'figmaUrl', description: 'Figma 파일 URL', required: true, }, { name: 'framework', description: '프레임워크 선택 (react 또는 vue)', required: true, }, { name: 'componentName', description: '컴포넌트 이름', required: true, }, ], }, { name: 'explore-design-system', description: '디자인 시스템 컴포넌트 탐색 가이드', arguments: [ { name: 'framework', description: '프레임워크 선택 (react 또는 vue)', required: false, }, ], }, ]; // Resources 정의 const resources = [ { uri: 'palette://design-system/react/components', name: 'React Design System Components', description: '@dealicious/design-system-react에서 사용 가능한 컴포넌트 목록', mimeType: 'application/json', }, { uri: 'palette://design-system/vue/components', name: 'Vue Design System Components', description: '@dealicious/design-system에서 사용 가능한 컴포넌트 목록', mimeType: 'application/json', }, ]; /** * MCP 서버를 생성하고 핸들러를 등록합니다. * Local 모드와 Remote 모드에서 공통으로 사용됩니다. */ export function createPaletteServer(config: ServerConfig = {}): Server { // 환경변수에서 설정 읽기 (config가 우선) if (config.figmaAccessToken) { process.env.FIGMA_ACCESS_TOKEN = config.figmaAccessToken; } if (config.githubToken) { process.env.GITHUB_TOKEN = config.githubToken; } if (config.figmaMcpServerUrl) { process.env.FIGMA_MCP_SERVER_URL = config.figmaMcpServerUrl; } // MCP 서버 초기화 (모든 capabilities 활성화) const server = new Server( { name: 'palette', version: '1.0.0', }, { capabilities: { tools: {}, // tools 기능 활성화 prompts: {}, // prompts 기능 활성화 resources: {}, // resources 기능 활성화 }, } ); // 서비스 초기화 // Remote 모드에서는 Figma Desktop MCP 클라이언트를 사용하지 않음 (로컬호스트 접근 불가) const useFigmaMcp = config.useFigmaMcp !== undefined ? config.useFigmaMcp : true; const figmaService = new FigmaService(useFigmaMcp, config.figmaMcpServerUrl); const designSystemService = new DesignSystemService(); const codeGenerator = new CodeGenerator(designSystemService); // ===== Tools 핸들러 ===== server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'convert_figma_to_react': { const { figmaUrl, nodeId, componentName, previewType = 'both' } = args as { figmaUrl: string; nodeId?: string; componentName: string; previewType?: PreviewType; }; const figmaData = await figmaService.getFigmaData(figmaUrl, nodeId); const files = await codeGenerator.generateAndSaveReactComponent( figmaData, componentName, figmaUrl, nodeId, previewType ); // 결과 메시지 구성 let fileList = `- 컴포넌트: \`${files.componentFile}\`\n`; if (files.htmlFile) { fileList += `- HTML 미리보기: \`${files.htmlFile}\`\n`; } if (files.imageFile) { fileList += `- 이미지 미리보기: \`${files.imageFile}\`\n`; } fileList += `- 메타데이터: \`${files.metadataFile}\``; const content: any[] = [ { type: 'text', text: `# React Component Generated\n\n**요청 ID:** \`${files.requestId}\`\n**저장 경로:** \`${files.folderPath}\`\n\n## 생성된 파일\n${fileList}\n\n## 컴포넌트 코드\n\n\`\`\`tsx\n${await readFile(files.componentFile, 'utf-8')}\n\`\`\``, }, ]; // 이미지가 있으면 이미지도 포함 if (files.imageFile) { try { const imageBuffer = await readFile(files.imageFile); content.push({ type: 'image', data: imageBuffer.toString('base64'), mimeType: 'image/png' }); } catch (error) { console.warn('이미지 읽기 실패:', error); } } return { content }; } case 'convert_figma_to_vue': { const { figmaUrl, nodeId, componentName, previewType = 'both' } = args as { figmaUrl: string; nodeId?: string; componentName: string; previewType?: PreviewType; }; const figmaData = await figmaService.getFigmaData(figmaUrl, nodeId); const files = await codeGenerator.generateAndSaveVueComponent( figmaData, componentName, figmaUrl, nodeId, previewType ); // 결과 메시지 구성 let fileList = `- 컴포넌트: \`${files.componentFile}\`\n`; if (files.htmlFile) { fileList += `- HTML 미리보기: \`${files.htmlFile}\`\n`; } if (files.imageFile) { fileList += `- 이미지 미리보기: \`${files.imageFile}\`\n`; } fileList += `- 메타데이터: \`${files.metadataFile}\``; const content: any[] = [ { type: 'text', text: `# Vue Component Generated\n\n**요청 ID:** \`${files.requestId}\`\n**저장 경로:** \`${files.folderPath}\`\n\n## 생성된 파일\n${fileList}\n\n## 컴포넌트 코드\n\n\`\`\`vue\n${await readFile(files.componentFile, 'utf-8')}\n\`\`\``, }, ]; // 이미지가 있으면 이미지도 포함 if (files.imageFile) { try { const imageBuffer = await readFile(files.imageFile); content.push({ type: 'image', data: imageBuffer.toString('base64'), mimeType: 'image/png' }); } catch (error) { console.warn('이미지 읽기 실패:', error); } } return { content }; } case 'list_design_system_components': { const { framework } = args as { framework: 'react' | 'vue' }; const components = await designSystemService.getAvailableComponents(framework); return { content: [ { type: 'text', text: `# Available ${framework} Components\n\n${components .map((comp) => `- **${comp.name}**: ${comp.description}`) .join('\n')}`, }, ], }; } case 'analyze_figma_file': { const { figmaUrl } = args as { figmaUrl: string }; const analysis = await figmaService.analyzeFigmaFile(figmaUrl); return { content: [ { type: 'text', text: `# Figma File Analysis\n\n${analysis}`, }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }); // ===== Prompts 핸들러 ===== server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'convert-design-to-component': { const figmaUrl = args?.figmaUrl || '<FIGMA_URL>'; const framework = args?.framework || 'react'; const componentName = args?.componentName || 'MyComponent'; return { description: 'Figma 디자인을 컴포넌트로 변환하는 프롬프트', messages: [ { role: 'user', content: { type: 'text', text: `Figma 디자인을 ${framework} 컴포넌트로 변환해주세요.\n\n- Figma URL: ${figmaUrl}\n- 컴포넌트 이름: ${componentName}\n\n디자인 시스템 컴포넌트를 최대한 활용해주세요.`, }, }, ], }; } case 'explore-design-system': { const framework = args?.framework || 'react'; return { description: '디자인 시스템 탐색 프롬프트', messages: [ { role: 'user', content: { type: 'text', text: `@dealicious/design-system${framework === 'react' ? '-react' : ''} 패키지에서 사용 가능한 컴포넌트 목록을 보여주세요. 각 컴포넌트의 사용법과 예제도 함께 알려주세요.`, }, }, ], }; } default: throw new Error(`Unknown prompt: ${name}`); } }); // ===== Resources 핸들러 ===== server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; switch (uri) { case 'palette://design-system/react/components': { const components = await designSystemService.getAvailableComponents('react'); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(components, null, 2), }, ], }; } case 'palette://design-system/vue/components': { const components = await designSystemService.getAvailableComponents('vue'); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(components, null, 2), }, ], }; } default: throw new Error(`Unknown resource: ${uri}`); } }); return server; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Opti-kjh/palatte'

If you have feedback or need assistance with the MCP directory API, please join our Discord server