Skip to main content
Glama
iamnotyk

TypeScript MCP Server Boilerplate

by iamnotyk
index.ts25.5 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { z } from 'zod' import { InferenceClient } from '@huggingface/inference' // Smithery 설정 스키마 export const configSchema = z.object({ hfToken: z .string() .optional() .describe('Hugging Face API 토큰 (이미지 생성 도구 사용 시 필요)') }) // Smithery 배포를 위한 createServer 함수 export default function createServer({ config }: { config: z.infer<typeof configSchema> }) { // 서버 인스턴스 생성 const server = new McpServer({ name: 'typescript-mcp-server', version: '1.0.0', capabilities: { tools: {}, resources: {}, prompts: {} } }) // 예시 도구: 인사하기 server.tool( 'greeting', { name: z.string().describe('인사할 사람의 이름'), language: z .enum(['ko', 'en']) .optional() .default('ko') .describe('인사 언어 (기본값: ko)') }, async ({ name, language }) => { const greeting = language === 'ko' ? `안녕하세요, ${name}님! 😊` : `Hello, ${name}! 👋` return { content: [ { type: 'text', text: greeting } ] } } ) // 예시 도구: 계산기 server.tool( 'calculator', { a: z.number().describe('첫 번째 숫자'), b: z.number().describe('두 번째 숫자'), operator: z .enum(['+', '-', '*', '/']) .describe('연산자 (+, -, *, /)') }, async ({ a, b, operator }) => { // 연산 수행 let result: number switch (operator) { case '+': result = a + b break case '-': result = a - b break case '*': result = a * b break case '/': if (b === 0) throw new Error('0으로 나눌 수 없습니다') result = a / b break default: throw new Error('지원하지 않는 연산자입니다') } return { content: [ { type: 'text', text: `${a} ${operator} ${b} = ${result}` } ] } } ) // TIME MCP 도구: 타임존별 현재 시간 조회 server.tool( 'get_time', { timeZone: z .string() .describe('IANA 타임존 이름 (예: America/New_York, Europe/London, Asia/Seoul)') }, async ({ timeZone }) => { try { const now = new Date() // 타임존 유효성 검사 const formatter = new Intl.DateTimeFormat('ko-KR', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }) const formattedTime = formatter.format(now) const utcTime = now.toISOString() // 타임존 오프셋 계산 const utcDate = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' })) const tzDate = new Date(now.toLocaleString('en-US', { timeZone })) const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60) const offsetHours = Math.floor(Math.abs(offset)) const offsetMinutes = Math.floor((Math.abs(offset) - offsetHours) * 60) const offsetSign = offset >= 0 ? '+' : '-' const offsetString = `UTC${offsetSign}${String(offsetHours).padStart(2, '0')}:${String(offsetMinutes).padStart(2, '0')}` const timeInfo = { timezone: timeZone, localTime: formattedTime, utcTime: utcTime, offset: offsetString, timestamp: now.getTime(), date: formattedTime.split(' ')[0], time: formattedTime.split(' ')[1] } return { content: [ { type: 'text', text: JSON.stringify(timeInfo, null, 2) } ] } } catch (error) { throw new Error( `유효하지 않은 타임존입니다: ${timeZone}. IANA 타임존 형식(예: America/New_York)을 사용해주세요.` ) } } ) // 이미지 생성 도구 server.tool( 'generate_image', { prompt: z.string().describe('이미지 생성을 위한 프롬프트') }, async ({ prompt }) => { try { // Hugging Face 토큰 확인 (config에서 가져옴) if (!config.hfToken) { throw new Error('hfToken 설정이 필요합니다. Smithery 설정에서 Hugging Face API 토큰을 입력해주세요.') } // Hugging Face Inference 클라이언트 생성 const client = new InferenceClient(config.hfToken) // 이미지 생성 요청 const imageBlob = await client.textToImage({ provider: 'auto', model: 'black-forest-labs/FLUX.1-schnell', inputs: prompt, parameters: { num_inference_steps: 5 } }) // Blob을 ArrayBuffer로 변환 후 base64 인코딩 const arrayBuffer = await ( imageBlob as unknown as Blob ).arrayBuffer() const buffer = Buffer.from(arrayBuffer) const base64Data = buffer.toString('base64') return { content: [ { type: 'image', data: base64Data, mimeType: 'image/png' } ], annotations: { audience: ['user'], priority: 0.9 } } } catch (error) { throw new Error( `이미지 생성 중 오류가 발생했습니다: ${ error instanceof Error ? error.message : '알 수 없는 오류' }` ) } } ) // Geocode MCP 도구: Nominatim OpenStreetMap API를 사용한 지오코딩 server.tool( 'geocode', { query: z .string() .describe('검색할 도시 이름 또는 주소 (예: "Seoul", "New York", "서울시 강남구")'), limit: z .number() .min(1) .max(40) .optional() .default(1) .describe('반환할 결과 수 (기본값: 1, 최대: 40)') }, async ({ query, limit }) => { try { const url = new URL('https://nominatim.openstreetmap.org/search') url.searchParams.set('q', query) url.searchParams.set('format', 'jsonv2') url.searchParams.set('limit', String(limit)) url.searchParams.set('addressdetails', '1') const response = await fetch(url.toString(), { headers: { 'User-Agent': 'typescript-mcp-server/1.0.0', 'Accept-Language': 'ko,en' } }) if (!response.ok) { throw new Error( `Nominatim API 오류: ${response.status} ${response.statusText}` ) } const results = await response.json() if (!results || results.length === 0) { return { content: [ { type: 'text', text: `"${query}"에 대한 검색 결과가 없습니다.` } ] } } // 위도와 경도 좌표를 명확하게 반환 const formatted = results.map((r: any) => ({ display_name: r.display_name, latitude: parseFloat(r.lat), longitude: parseFloat(r.lon), coordinates: { lat: parseFloat(r.lat), lon: parseFloat(r.lon) }, place_id: r.place_id, osm_type: r.osm_type, osm_id: r.osm_id, type: r.type, category: r.category, importance: r.importance, address: r.address || null, boundingbox: r.boundingbox || null })) return { content: [ { type: 'text', text: JSON.stringify(formatted, null, 2) } ] } } catch (error) { throw new Error( `지오코딩 중 오류가 발생했습니다: ${ error instanceof Error ? error.message : '알 수 없는 오류' }` ) } } ) // 날씨 정보 도구: Open-Meteo API 사용 server.tool( 'get_weather', { latitude: z.number().min(-90).max(90).describe('위도 (WGS84)'), longitude: z.number().min(-180).max(180).describe('경도 (WGS84)'), timezone: z .string() .optional() .default('auto') .describe('시간대 (기본값: auto - 자동 감지)'), forecast_days: z .number() .min(1) .max(16) .optional() .default(3) .describe('예보 일수 (기본값: 3, 최대: 16)') }, async ({ latitude, longitude, timezone, forecast_days }) => { const url = new URL('https://api.open-meteo.com/v1/forecast') url.searchParams.set('latitude', String(latitude)) url.searchParams.set('longitude', String(longitude)) url.searchParams.set('timezone', timezone) url.searchParams.set('forecast_days', String(forecast_days)) url.searchParams.set( 'current', 'temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m' ) url.searchParams.set( 'daily', 'temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code' ) const response = await fetch(url.toString(), { headers: { 'User-Agent': 'typescript-mcp-server/1.0.0' } }) if (!response.ok) { throw new Error(`Open-Meteo API 오류: ${response.status}`) } const data = await response.json() if (data.error) { throw new Error( `Open-Meteo API 오류: ${data.reason || '알 수 없는 오류'}` ) } const formatted = { location: { latitude: data.latitude, longitude: data.longitude, timezone: data.timezone, elevation: data.elevation }, current: data.current ? { temperature: data.current.temperature_2m, humidity: data.current.relative_humidity_2m, weather_code: data.current.weather_code, wind_speed: data.current.wind_speed_10m, time: data.current.time } : null, daily: data.daily ? { time: data.daily.time, temperature_max: data.daily.temperature_2m_max, temperature_min: data.daily.temperature_2m_min, precipitation: data.daily.precipitation_sum, weather_code: data.daily.weather_code } : null, units: { temperature: data.current_units?.temperature_2m || '°C', humidity: data.current_units?.relative_humidity_2m || '%', wind_speed: data.current_units?.wind_speed_10m || 'km/h', precipitation: data.daily_units?.precipitation_sum || 'mm' } } return { content: [ { type: 'text', text: JSON.stringify(formatted, null, 2) } ] } } ) // 서버 정보 및 도구 목록 리소스 server.resource( 'server://info', 'server://info', { name: '서버 정보 및 도구 목록', description: '현재 서버 정보와 사용 가능한 모든 도구 정보를 반환합니다', mimeType: 'application/json' }, async () => { // 사용 가능한 도구 정보 const availableTools = [ { name: 'greeting', description: '인사하기 - 이름과 언어를 입력받아 인사 메시지를 반환합니다', parameters: { name: { type: 'string', description: '인사할 사람의 이름', required: true }, language: { type: 'enum', values: ['ko', 'en'], description: '인사 언어 (기본값: ko)', required: false, default: 'ko' } } }, { name: 'calculator', description: '계산기 - 두 개의 숫자와 연산자를 입력받아 사칙연산 결과를 반환합니다', parameters: { a: { type: 'number', description: '첫 번째 숫자', required: true }, b: { type: 'number', description: '두 번째 숫자', required: true }, operator: { type: 'enum', values: ['+', '-', '*', '/'], description: '연산자 (+, -, *, /)', required: true } } }, { name: 'get_time', description: 'TIME MCP - 타임존을 입력받아 해당 지역의 현재 시간 정보를 반환합니다', parameters: { timeZone: { type: 'string', description: 'IANA 타임존 이름 (예: America/New_York, Europe/London, Asia/Seoul)', required: true } } }, { name: 'generate_image', description: '이미지 생성 - 프롬프트를 입력받아 AI로 생성한 이미지를 반환합니다 (Hugging Face API 사용)', parameters: { prompt: { type: 'string', description: '이미지 생성을 위한 프롬프트', required: true } } }, { name: 'geocode', description: 'Geocode MCP - 도시 이름이나 주소를 입력받아 위도/경도 좌표를 반환합니다 (Nominatim OpenStreetMap API 사용)', parameters: { query: { type: 'string', description: '검색할 도시 이름 또는 주소 (예: "Seoul", "New York", "서울시 강남구")', required: true }, limit: { type: 'number', description: '반환할 결과 수 (기본값: 1, 최대: 40)', required: false, default: 1, min: 1, max: 40 } } }, { name: 'get_weather', description: '날씨 정보 - 위도/경도 좌표를 입력받아 해당 지역의 날씨 정보를 반환합니다 (Open-Meteo API 사용)', parameters: { latitude: { type: 'number', description: '위도 (WGS84)', required: true, min: -90, max: 90 }, longitude: { type: 'number', description: '경도 (WGS84)', required: true, min: -180, max: 180 }, timezone: { type: 'string', description: '시간대 (기본값: auto - 자동 감지)', required: false, default: 'auto' }, forecast_days: { type: 'number', description: '예보 일수 (기본값: 3, 최대: 16)', required: false, default: 3, min: 1, max: 16 } } } ] const serverInfo = { server: { name: 'typescript-mcp-server', version: '1.0.0', description: 'TypeScript MCP Server 보일러플레이트', timestamp: new Date().toISOString(), uptime: process.uptime(), nodeVersion: process.version, platform: process.platform, architecture: process.arch }, capabilities: { tools: availableTools.length, resources: 1, prompts: 1 }, tools: availableTools, resources: [ { uri: 'server://info', name: '서버 정보 및 도구 목록', description: '현재 서버 정보와 사용 가능한 모든 도구 정보를 반환합니다' } ], prompts: [ { name: 'code_review', description: 'Code Review MCP - 코드를 입력받아 미리 정의된 프롬프트 템플릿을 결합하여 상세한 코드 리뷰를 제공합니다', parameters: { code: { type: 'string', description: '리뷰할 코드', required: true }, language: { type: 'string', description: '코드 언어 (예: typescript, javascript, python, java 등, 기본값: auto)', required: false, default: 'auto' }, focus_areas: { type: 'string', description: '리뷰에 집중할 영역 (쉼표로 구분: quality,performance,security,maintainability,best_practices,all, 기본값: all)', required: false, default: 'all' }, include_suggestions: { type: 'string', description: '개선 제안 포함 여부 (true/false, 기본값: true)', required: false, default: 'true' } } } ] } return { contents: [ { uri: 'server://info', mimeType: 'application/json', text: JSON.stringify(serverInfo, null, 2) } ] } } ) // Code Review MCP 프롬프트: 코드 리뷰를 위한 상세한 프롬프트 템플릿 server.prompt( 'code_review', 'Code Review Request', { code: z .string() .describe('리뷰할 코드'), language: z .string() .optional() .describe('코드 언어 (예: typescript, javascript, python, java 등, 기본값: auto - 자동 감지)'), focus_areas: z .string() .optional() .describe('리뷰에 집중할 영역 (쉼표로 구분: quality,performance,security,maintainability,best_practices,all, 기본값: all)'), include_suggestions: z .string() .optional() .describe('개선 제안 포함 여부 (true/false, 기본값: true)') }, async ({ code, language, focus_areas, include_suggestions }) => { // 기본값 설정 및 파싱 const finalLanguage = language ?? 'auto' const finalFocusAreas = focus_areas ? focus_areas.split(',').map(area => area.trim()) : ['all'] const finalIncludeSuggestions = include_suggestions ? include_suggestions.toLowerCase() === 'true' : true // 미리 정의된 프롬프트 템플릿 const reviewTemplate = { header: '다음 코드를 상세히 분석하고 리뷰해주세요.', sections: [] as string[], footer: finalIncludeSuggestions ? '\n각 항목에 대해 구체적인 개선 제안과 예시 코드를 포함해주세요.' : '' } // 집중 영역에 따라 섹션 추가 if (finalFocusAreas.includes('all') || finalFocusAreas.includes('quality')) { reviewTemplate.sections.push( '## 1. 코드 품질 평가\n' + '- 코드 가독성 및 명확성\n' + '- 네이밍 컨벤션 준수 여부\n' + '- 코드 구조 및 조직화\n' + '- 중복 코드 존재 여부' ) } if (finalFocusAreas.includes('all') || finalFocusAreas.includes('performance')) { reviewTemplate.sections.push( '## 2. 성능 최적화\n' + '- 알고리즘 효율성\n' + '- 불필요한 연산 또는 메모리 사용\n' + '- 비동기 처리 최적화 (해당되는 경우)\n' + '- 데이터베이스 쿼리 최적화 (해당되는 경우)' ) } if (finalFocusAreas.includes('all') || finalFocusAreas.includes('security')) { reviewTemplate.sections.push( '## 3. 보안 고려사항\n' + '- 입력 검증 및 sanitization\n' + '- SQL Injection, XSS 등 취약점\n' + '- 인증 및 권한 관리\n' + '- 민감한 정보 처리 (API 키, 비밀번호 등)\n' + '- 에러 처리 및 정보 노출 방지' ) } if (finalFocusAreas.includes('all') || finalFocusAreas.includes('maintainability')) { reviewTemplate.sections.push( '## 4. 유지보수성\n' + '- 코드 모듈화 및 재사용성\n' + '- 테스트 가능성\n' + '- 문서화 및 주석\n' + '- 의존성 관리' ) } if (finalFocusAreas.includes('all') || finalFocusAreas.includes('best_practices')) { reviewTemplate.sections.push( '## 5. 모범 사례 및 표준 준수\n' + '- 언어별 코딩 표준 준수\n' + '- 디자인 패턴 적용 (해당되는 경우)\n' + '- SOLID 원칙 준수 (해당되는 경우)\n' + '- 에러 핸들링 패턴' ) } // 언어 정보 추가 const languageInfo = finalLanguage !== 'auto' ? `\n**코드 언어**: ${finalLanguage}\n` : '\n**코드 언어**: 자동 감지\n' // 최종 프롬프트 구성 const promptText = reviewTemplate.header + languageInfo + '\n' + reviewTemplate.sections.join('\n\n') + '\n\n' + '---\n\n' + '**리뷰할 코드:**\n\n' + '```' + (finalLanguage !== 'auto' ? finalLanguage : '') + '\n' + code + '\n```' + reviewTemplate.footer return { messages: [ { role: 'user', content: { type: 'text', text: promptText } } ] } } ) // Smithery 배포를 위해 MCP 서버 객체 반환 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/iamnotyk/mcp-server-251215'

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