Skip to main content
Glama
worbsmse1029-cpu

TypeScript MCP Server Boilerplate

index.ts30.5 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { z } from 'zod' import { HfInference } from '@huggingface/inference' // 설정 스키마 (선택적: 사용자 설정이 필요한 경우) export const configSchema = z.object({ hfToken: z.string().optional().describe('Hugging Face API 토큰 (이미지 생성 기능에 필요)') }) // Required: Export default createServer function export default function createServer({ config: userConfig }: { config?: { hfToken?: string } } = {}) { // Create server instance const server = new McpServer({ name: 'my-mcp-server', version: '1.0.0' }) // HF 토큰 설정 (config에서 받거나 환경 변수에서) const hfToken = userConfig?.hfToken || process.env.HF_TOKEN server.registerTool( 'greet', { description: '이름과 언어를 입력하면 인사말을 반환합니다.', inputSchema: z.object({ name: z.string().describe('인사할 사람의 이름'), language: z .enum(['ko', 'en']) .optional() .default('en') .describe('인사 언어 (기본값: en)') }), outputSchema: z.object({ content: z .array( z.object({ type: z.literal('text'), text: z.string().describe('인사말') }) ) .describe('인사말') }) }, async ({ name, language }) => { const greeting = language === 'ko' ? `안녕하세요, ${name}님!` : `Hey there, ${name}! 👋 Nice to meet you!` return { content: [ { type: 'text' as const, text: greeting } ], structuredContent: { content: [ { type: 'text' as const, text: greeting } ] } } } ) server.registerTool( 'calculator', { description: '두 개의 숫자와 연산자를 입력받아 사칙연산 결과를 반환합니다.', inputSchema: z.object({ num1: z.number().describe('첫 번째 숫자'), num2: z.number().describe('두 번째 숫자'), operator: z .enum(['+', '-', '*', '/']) .describe('연산자 (+, -, *, /)') }), outputSchema: z.object({ content: z .array( z.object({ type: z.literal('text'), text: z.string().describe('계산 결과') }) ) .describe('계산 결과') }) }, async ({ num1, num2, operator }) => { let result: number let resultText: string switch (operator) { case '+': result = num1 + num2 resultText = `${num1} + ${num2} = ${result}` break case '-': result = num1 - num2 resultText = `${num1} - ${num2} = ${result}` break case '*': result = num1 * num2 resultText = `${num1} × ${num2} = ${result}` break case '/': if (num2 === 0) { resultText = '오류: 0으로 나눌 수 없습니다.' } else { result = num1 / num2 resultText = `${num1} ÷ ${num2} = ${result}` } break default: resultText = '오류: 지원하지 않는 연산자입니다.' } return { content: [ { type: 'text' as const, text: resultText } ], structuredContent: { content: [ { type: 'text' as const, text: resultText } ] } } } ) server.registerTool( 'time', { description: '시간대를 입력받아 해당 시간대의 현재 시각을 반환합니다.', inputSchema: z.object({ timezone: z .string() .regex(/^UTC[+-]\d+$/, '시간대는 UTC+숫자 또는 UTC-숫자 형식이어야 합니다.') .describe('시간대 (예: UTC+9, UTC+0, UTC-5)') }), outputSchema: z.object({ content: z .array( z.object({ type: z.literal('text'), text: z.string().describe('현재 시각') }) ) .describe('현재 시각') }) }, async ({ timezone }) => { // UTC+숫자 또는 UTC-숫자 형식 파싱 const match = timezone.match(/^UTC([+-])(\d+)$/) if (!match) { return { content: [ { type: 'text' as const, text: '오류: 잘못된 시간대 형식입니다. UTC+숫자 또는 UTC-숫자 형식을 사용해주세요.' } ], structuredContent: { content: [ { type: 'text' as const, text: '오류: 잘못된 시간대 형식입니다. UTC+숫자 또는 UTC-숫자 형식을 사용해주세요.' } ] } } } const sign = match[1] === '+' ? 1 : -1 const offsetHours = parseInt(match[2], 10) // 현재 UTC 시간 가져오기 const now = new Date() const utcTime = now.getTime() + now.getTimezoneOffset() * 60 * 1000 // 시간대 오프셋 적용 const targetTime = new Date(utcTime + offsetHours * 60 * 60 * 1000 * sign) // 날짜와 시간 포맷팅 const year = targetTime.getUTCFullYear() const month = String(targetTime.getUTCMonth() + 1).padStart(2, '0') const day = String(targetTime.getUTCDate()).padStart(2, '0') const hours = String(targetTime.getUTCHours()).padStart(2, '0') const minutes = String(targetTime.getUTCMinutes()).padStart(2, '0') const seconds = String(targetTime.getUTCSeconds()).padStart(2, '0') const timeString = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` const resultText = `${timezone} 시간대의 현재 시각: ${timeString}` return { content: [ { type: 'text' as const, text: resultText } ], structuredContent: { content: [ { type: 'text' as const, text: resultText } ] } } } ) server.registerTool( 'geocode', { description: '도시 이름이나 주소를 입력받아서 위도와 경도 좌표를 반환합니다.', inputSchema: z.object({ query: z .string() .describe('검색할 도시 이름이나 주소 (예: "서울", "New York", "서울시 강남구")') }), outputSchema: z.object({ content: z .array( z.object({ type: z.literal('text'), text: z.string().describe('위도와 경도 좌표 정보') }) ) .describe('위도와 경도 좌표') }) }, async ({ query }) => { try { // Nominatim API 호출 const encodedQuery = encodeURIComponent(query) const url = `https://nominatim.openstreetmap.org/search?q=${encodedQuery}&format=jsonv2&limit=1&addressdetails=1` const response = await fetch(url, { headers: { 'User-Agent': 'MCP-Geocode-Tool/1.0' } }) if (!response.ok) { throw new Error(`API 요청 실패: ${response.status} ${response.statusText}`) } const data = await response.json() if (!Array.isArray(data) || data.length === 0) { return { content: [ { type: 'text' as const, text: `"${query}"에 대한 검색 결과를 찾을 수 없습니다.` } ], structuredContent: { content: [ { type: 'text' as const, text: `"${query}"에 대한 검색 결과를 찾을 수 없습니다.` } ] } } } const result = data[0] const lat = parseFloat(result.lat) const lon = parseFloat(result.lon) const displayName = result.display_name || query const resultText = `위치: ${displayName}\n위도: ${lat}\n경도: ${lon}` return { content: [ { type: 'text' as const, text: resultText } ], structuredContent: { content: [ { type: 'text' as const, text: resultText } ] } } } catch (error) { const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.' return { content: [ { type: 'text' as const, text: `오류: ${errorMessage}` } ], structuredContent: { content: [ { type: 'text' as const, text: `오류: ${errorMessage}` } ] } } } } ) server.registerTool( 'get-weather', { description: '위도와 경도 좌표, 예보 기간을 입력받아서 해당 위치의 현재 날씨와 예보 정보를 제공합니다.', inputSchema: z.object({ latitude: z .number() .min(-90) .max(90) .describe('위도 (latitude, -90 ~ 90)'), longitude: z .number() .min(-180) .max(180) .describe('경도 (longitude, -180 ~ 180)'), forecastDays: z .number() .int() .min(1) .max(16) .optional() .default(7) .describe('예보 기간 (일 단위, 1~16일, 기본값: 7일)') }), outputSchema: z.object({ content: z .array( z.object({ type: z.literal('text'), text: z.string().describe('날씨 정보') }) ) .describe('날씨 정보') }) }, async ({ latitude, longitude, forecastDays = 7 }) => { try { // Open-Meteo API 호출 const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true&hourly=temperature_2m,precipitation,weathercode&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode&forecast_days=${forecastDays}&timezone=auto` const response = await fetch(url) if (!response.ok) { throw new Error(`API 요청 실패: ${response.status} ${response.statusText}`) } const data = await response.json() if (!data || !data.current_weather) { throw new Error('날씨 데이터를 가져올 수 없습니다.') } // 현재 날씨 정보 const current = data.current_weather const currentTemp = current.temperature const currentWeatherCode = current.weathercode const currentWindSpeed = current.windspeed const currentWindDirection = current.winddirection // 날씨 코드를 텍스트로 변환하는 함수 const getWeatherDescription = (code: number): string => { const weatherCodes: Record<number, string> = { 0: '맑음', 1: '대체로 맑음', 2: '부분적으로 흐림', 3: '흐림', 45: '안개', 48: '서리 안개', 51: '약한 이슬비', 53: '중간 이슬비', 55: '강한 이슬비', 56: '약한 동결 이슬비', 57: '강한 동결 이슬비', 61: '약한 비', 63: '중간 비', 65: '강한 비', 66: '약한 동결 비', 67: '강한 동결 비', 71: '약한 눈', 73: '중간 눈', 75: '강한 눈', 77: '눈알', 80: '약한 소나기', 81: '중간 소나기', 82: '강한 소나기', 85: '약한 눈 소나기', 86: '강한 눈 소나기', 95: '뇌우', 96: '우박을 동반한 뇌우', 99: '강한 우박을 동반한 뇌우' } return weatherCodes[code] || `날씨 코드: ${code}` } let resultText = `=== 현재 날씨 ===\n` resultText += `온도: ${currentTemp}°C\n` resultText += `날씨: ${getWeatherDescription(currentWeatherCode)}\n` resultText += `풍속: ${currentWindSpeed} km/h\n` resultText += `풍향: ${currentWindDirection}°\n\n` // 일별 예보 정보 if (data.daily && data.daily.time) { resultText += `=== ${forecastDays}일 예보 ===\n` const daily = data.daily const times = daily.time const maxTemps = daily.temperature_2m_max || [] const minTemps = daily.temperature_2m_min || [] const precipitations = daily.precipitation_sum || [] const weatherCodes = daily.weathercode || [] for (let i = 0; i < Math.min(times.length, forecastDays); i++) { const date = new Date(times[i]) const dateStr = `${date.getMonth() + 1}/${date.getDate()}` resultText += `\n${dateStr} (${times[i]})\n` resultText += ` 날씨: ${getWeatherDescription(weatherCodes[i])}\n` resultText += ` 최고: ${maxTemps[i]}°C / 최저: ${minTemps[i]}°C\n` if (precipitations[i] > 0) { resultText += ` 강수량: ${precipitations[i]} mm\n` } } } return { content: [ { type: 'text' as const, text: resultText } ], structuredContent: { content: [ { type: 'text' as const, text: resultText } ] } } } catch (error) { const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.' return { content: [ { type: 'text' as const, text: `오류: ${errorMessage}` } ], structuredContent: { content: [ { type: 'text' as const, text: `오류: ${errorMessage}` } ] } } } } ) // 코드 리뷰 프롬프트 템플릿 const codeReviewPromptTemplate = `다음 코드를 리뷰해주세요. 다음 항목들을 중점적으로 검토해주세요: 1. **코드 품질** - 가독성과 유지보수성 - 네이밍 컨벤션 - 코드 구조와 조직화 2. **성능** - 잠재적인 성능 병목 - 최적화 가능한 부분 - 메모리 사용 효율성 3. **보안** - 보안 취약점 - 입력 검증 - 에러 처리 4. **모범 사례** - 언어별 베스트 프랙티스 준수 여부 - 디자인 패턴 적용 - 테스트 가능성 5. **개선 제안** - 구체적인 개선 방안 - 리팩토링 제안 - 대안 코드 예시 **리뷰할 코드:** \`\`\` {code} \`\`\` 위 코드에 대한 상세한 리뷰를 제공해주세요.` // 코드 리뷰 프롬프트 등록 server.registerPrompt( 'code-review', { description: '코드를 입력받아 코드 리뷰를 위한 프롬프트를 생성합니다.', argsSchema: { code: z.string().describe('리뷰할 코드'), language: z .string() .optional() .describe('코드 언어 (선택사항, 예: typescript, javascript, python 등)'), focus: z .string() .optional() .describe('특별히 집중할 리뷰 영역 (선택사항, 예: performance, security, readability)') } }, async (args) => { const code = args.code || '' const language = args.language || '' const focus = args.focus || '' // 언어별 추가 지침 const languageGuidelines: Record<string, string> = { typescript: '\n\n**TypeScript 특화 검토 사항:**\n- 타입 안정성과 타입 추론\n- 제네릭 사용의 적절성\n- 인터페이스와 타입 정의의 명확성', javascript: '\n\n**JavaScript 특화 검토 사항:**\n- ES6+ 기능 활용\n- 비동기 처리 (Promise, async/await)\n- 스코프와 클로저 사용', python: '\n\n**Python 특화 검토 사항:**\n- PEP 8 스타일 가이드 준수\n- 리스트 컴프리헨션 활용\n- 예외 처리와 컨텍스트 매니저 사용', java: '\n\n**Java 특화 검토 사항:**\n- 객체지향 설계 원칙\n- 예외 처리 전략\n- 컬렉션 프레임워크 활용', go: '\n\n**Go 특화 검토 사항:**\n- 에러 처리 패턴\n- 고루틴과 채널 사용\n- 인터페이스 설계' } // 집중 영역별 추가 지침 const focusGuidelines: Record<string, string> = { performance: '\n\n**성능 최적화 집중 검토:**\n- 알고리즘 시간 복잡도 분석\n- 불필요한 반복문이나 중첩 루프\n- 캐싱 가능한 연산\n- 데이터베이스 쿼리 최적화', security: '\n\n**보안 집중 검토:**\n- SQL 인젝션 방지\n- XSS 공격 방지\n- 인증 및 권한 검증\n- 민감 정보 노출 방지', readability: '\n\n**가독성 집중 검토:**\n- 변수와 함수명의 명확성\n- 주석의 적절성\n- 코드 길이와 복잡도\n- 일관된 코딩 스타일' } // 프롬프트 생성 let prompt = codeReviewPromptTemplate.replace('{code}', code) if (language && languageGuidelines[language.toLowerCase()]) { prompt += languageGuidelines[language.toLowerCase()] } if (focus && focusGuidelines[focus.toLowerCase()]) { prompt += focusGuidelines[focus.toLowerCase()] } return { messages: [ { role: 'user', content: { type: 'text', text: prompt } } ] } } ) // Blob을 base64로 변환하는 헬퍼 함수 async function blobToBase64(blob: Blob): Promise<string> { const buffer = await blob.arrayBuffer() const base64 = Buffer.from(buffer).toString('base64') return base64 } // 이미지 생성 도구 등록 server.registerTool( 'generate-image', { description: '프롬프트를 입력받아 AI로 이미지를 생성합니다.', inputSchema: z.object({ prompt: z.string().describe('이미지 생성을 위한 텍스트 프롬프트') }), outputSchema: z.object({ content: z .array( z.object({ type: z.literal('text'), text: z.string().describe('base64로 인코딩된 이미지 데이터 URI') }) ) .describe('생성된 이미지') }) }, async ({ prompt }) => { try { // Hugging Face API 토큰 확인 if (!hfToken) { throw new Error('HF_TOKEN 환경 변수가 설정되지 않았습니다. configSchema의 hfToken을 설정하거나 HF_TOKEN 환경 변수를 설정해주세요.') } // Hugging Face Inference 클라이언트 초기화 const hfClient = new HfInference(hfToken) // Hugging Face Inference API를 사용하여 이미지 생성 let imageBlob: Blob try { const result: any = await hfClient.textToImage({ model: 'black-forest-labs/FLUX.1-schnell', inputs: prompt, parameters: { num_inference_steps: 5 } }) // 결과가 Blob인지 확인하고 변환 imageBlob = result instanceof Blob ? result : new Blob([result]) } catch (apiError) { // API 오류를 더 자세히 로깅 const apiErrorMessage = apiError instanceof Error ? apiError.message : String(apiError) console.error('Hugging Face API error:', apiErrorMessage) throw new Error(`Hugging Face API 호출 실패: ${apiErrorMessage}. HF_TOKEN이 올바르게 설정되어 있는지 확인해주세요.`) } // Blob을 base64로 변환 const base64Data = await blobToBase64(imageBlob) // 데이터 URI 형식으로 변환 const dataUri = `data:image/png;base64,${base64Data}` return { content: [ { type: 'text' as const, text: dataUri } ], structuredContent: { content: [ { type: 'text' as const, text: dataUri } ] } } } catch (error) { const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.' return { content: [ { type: 'text' as const, text: `오류: ${errorMessage}` } ], structuredContent: { content: [ { type: 'text' as const, text: `오류: ${errorMessage}` } ] } } } } ) // 서버 정보와 도구 목록을 반환하는 리소스 server.resource( 'server-info', 'server://info', { description: '현재 서버 정보와 사용 가능한 도구 목록', mimeType: 'application/json' }, async () => { const serverInfo = { server: { name: 'my-mcp-server', version: '1.0.0', uptime: process.uptime(), timestamp: new Date().toISOString() }, tools: [ { name: 'greet', description: '이름과 언어를 입력하면 인사말을 반환합니다.', input: { name: 'string - 인사할 사람의 이름', language: 'enum["ko", "en"] (optional, default: "en") - 인사 언어' } }, { name: 'calculator', description: '두 개의 숫자와 연산자를 입력받아 사칙연산 결과를 반환합니다.', input: { num1: 'number - 첫 번째 숫자', num2: 'number - 두 번째 숫자', operator: 'enum["+", "-", "*", "/"] - 연산자' } }, { name: 'time', description: '시간대를 입력받아 해당 시간대의 현재 시각을 반환합니다.', input: { timezone: 'string - 시간대 (예: UTC+9, UTC+0, UTC-5)' } }, { name: 'geocode', description: '도시 이름이나 주소를 입력받아서 위도와 경도 좌표를 반환합니다.', input: { query: 'string - 검색할 도시 이름이나 주소' } }, { name: 'get-weather', description: '위도와 경도 좌표, 예보 기간을 입력받아서 해당 위치의 현재 날씨와 예보 정보를 제공합니다.', input: { latitude: 'number (-90 ~ 90) - 위도', longitude: 'number (-180 ~ 180) - 경도', forecastDays: 'number (1~16, optional, default: 7) - 예보 기간 (일 단위)' } }, { name: 'generate-image', description: '프롬프트를 입력받아 AI로 이미지를 생성합니다.', input: { prompt: 'string - 이미지 생성을 위한 텍스트 프롬프트' } } ] } return { contents: [ { uri: 'server://info', mimeType: 'application/json', text: JSON.stringify(serverInfo, null, 2) } ] } } ) // Must return the MCP server object return server.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/worbsmse1029-cpu/mcp-server'

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