import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
import { InferenceClient } from '@huggingface/inference'
// Smithery 배포를 위한 설정 스키마 (Optional)
export const configSchema = z.object({
HF_TOKEN: z.string().optional().describe('Hugging Face API 토큰 (이미지 생성 도구 사용 시 필요)')
})
// 설정 타입 정의
type Config = z.infer<typeof configSchema>
// Smithery 배포를 위한 createServer 함수 (Required)
export default function createServer({ config }: { config?: Config } = {}) {
// Create server instance
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0'
})
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: {
type: 'object',
properties: {
a: {
type: 'number',
description: '첫 번째 숫자'
},
b: {
type: 'number',
description: '두 번째 숫자'
},
operation: {
type: 'string',
enum: ['add', 'subtract', 'multiply', 'divide'],
description: '수행할 연산 (add: 덧셈, subtract: 뺄셈, multiply: 곱셈, divide: 나눗셈)'
}
},
required: ['a', 'b', 'operation']
} as any,
outputSchema: {
type: 'object',
properties: {
content: {
type: 'array',
items: {
type: 'object',
properties: {
type: {
type: 'string',
const: 'text'
},
text: {
type: 'string',
description: '계산 결과'
}
},
required: ['type', 'text']
},
description: '계산 결과'
}
},
required: ['content']
} as any
},
async ({ a, b, operation }: { a: number; b: number; operation: 'add' | 'subtract' | 'multiply' | 'divide' }) => {
let result: number
let operationSymbol: string
switch (operation) {
case 'add':
result = a + b
operationSymbol = '+'
break
case 'subtract':
result = a - b
operationSymbol = '-'
break
case 'multiply':
result = a * b
operationSymbol = '×'
break
case 'divide':
if (b === 0) {
throw new Error('0으로 나눌 수 없습니다')
}
result = a / b
operationSymbol = '÷'
break
default:
throw new Error('지원하지 않는 연산입니다')
}
const resultText = `${a} ${operationSymbol} ${b} = ${result}`
return {
content: [
{
type: 'text' as const,
text: resultText
}
],
structuredContent: {
content: [
{
type: 'text' as const,
text: resultText
}
]
}
}
}
)
server.registerTool(
'geocode',
{
description: '도시 이름이나 주소를 입력받아 위도와 경도 좌표를 반환합니다. Nominatim OpenStreetMap API를 사용합니다.',
inputSchema: z.object({
query: z.string().describe('검색할 도시 이름이나 주소 (예: "New York", "서울", "1600 Amphitheatre Parkway, Mountain View, CA")')
})
},
async ({ query }: { query: string }) => {
try {
// Nominatim API 호출
const encodedQuery = encodeURIComponent(query)
const url = `https://nominatim.openstreetmap.org/search?q=${encodedQuery}&format=json&limit=1`
const response = await fetch(url, {
headers: {
'User-Agent': 'MCP-Server/1.0' // Nominatim은 User-Agent를 요구합니다
}
})
if (!response.ok) {
throw new Error(`Nominatim API 요청 실패: ${response.status} ${response.statusText}`)
}
const data = await response.json()
if (!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}\n좌표: (${lat}, ${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 : '알 수 없는 오류가 발생했습니다'
throw new Error(`지오코딩 실패: ${errorMessage}`)
}
}
)
server.registerTool(
'get-weather',
{
description: '위도와 경도 좌표, 예보 기간을 입력받아 해당 위치의 현재 날씨와 예보 정보를 제공합니다. Open-Meteo Weather API를 사용합니다.',
inputSchema: z.object({
latitude: z.number().min(-90).max(90).describe('위도 (-90 ~ 90)'),
longitude: z.number().min(-180).max(180).describe('경도 (-180 ~ 180)'),
forecastDays: z
.number()
.int()
.min(1)
.max(16)
.optional()
.default(7)
.describe('예보 기간 (일 단위, 1~16일, 기본값: 7일)')
})
},
async ({
latitude,
longitude,
forecastDays = 7
}: {
latitude: number
longitude: number
forecastDays?: number
}) => {
try {
// Open-Meteo API 호출
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t_weather=true&hourly=temperature_2m,precipitation,wind_speed_10m&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code&forecast_days=${forecastDays}&timezone=auto`
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Open-Meteo API 요청 실패: ${response.status} ${response.statusText}`)
}
const data = await response.json()
if (!data) {
throw new Error('날씨 데이터를 가져올 수 없습니다')
}
// 현재 날씨 정보
const current = data.current_weather
const currentTemp = current?.temperature_2m || 'N/A'
const currentWindSpeed = current?.windspeed || 'N/A'
const weatherCode = current?.weathercode || 'N/A'
// 일일 예보 정보
const daily = data.daily
const dailyForecast: string[] = []
if (daily && daily.time && daily.time.length > 0) {
for (let i = 0; i < Math.min(daily.time.length, forecastDays); i++) {
const date = daily.time[i]
const maxTemp = daily.temperature_2m_max?.[i] ?? 'N/A'
const minTemp = daily.temperature_2m_min?.[i] ?? 'N/A'
const precipitation = daily.precipitation_sum?.[i] ?? 'N/A'
const code = daily.weather_code?.[i] ?? 'N/A'
dailyForecast.push(
`${date}: 최고 ${maxTemp}°C / 최저 ${minTemp}°C, 강수량 ${precipitation}mm (날씨코드: ${code})`
)
}
}
// 날씨 코드 설명 (간단한 매핑)
const weatherCodeMap: 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: '강한 우박과 함께하는 뇌우'
}
const weatherDescription =
typeof weatherCode === 'number' ? weatherCodeMap[weatherCode] || '알 수 없음' : 'N/A'
const resultText = `📍 위치: 위도 ${latitude}, 경도 ${longitude}
🌡️ 현재 날씨:
- 온도: ${currentTemp}°C
- 풍속: ${currentWindSpeed} km/h
- 날씨: ${weatherDescription} (코드: ${weatherCode})
📅 ${forecastDays}일 예보:
${dailyForecast.join('\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 : '알 수 없는 오류가 발생했습니다'
throw new Error(`날씨 정보 조회 실패: ${errorMessage}`)
}
}
)
// 서버 정보 및 도구 목록 리소스
server.registerResource(
'server-info',
'server://info',
{
title: '서버 정보',
description: '현재 MCP 서버 정보와 사용 가능한 도구 목록',
mimeType: 'application/json'
},
async () => {
const serverInfo = {
server: {
name: 'my-mcp-server',
version: '1.0.0',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
nodeVersion: process.version,
platform: process.platform
},
tools: [
{
name: 'greet',
description: '이름과 언어를 입력하면 인사말을 반환합니다.',
parameters: {
name: {
type: 'string',
description: '인사할 사람의 이름'
},
language: {
type: 'string',
enum: ['ko', 'en'],
optional: true,
default: 'en',
description: '인사 언어 (기본값: en)'
}
}
},
{
name: 'calculator',
description: '두 개의 숫자와 연산자를 입력받아 사칙연산 결과를 반환합니다.',
parameters: {
a: {
type: 'number',
description: '첫 번째 숫자'
},
b: {
type: 'number',
description: '두 번째 숫자'
},
operation: {
type: 'string',
enum: ['add', 'subtract', 'multiply', 'divide'],
description: '수행할 연산 (add: 덧셈, subtract: 뺄셈, multiply: 곱셈, divide: 나눗셈)'
}
}
},
{
name: 'geocode',
description: '도시 이름이나 주소를 입력받아 위도와 경도 좌표를 반환합니다. Nominatim OpenStreetMap API를 사용합니다.',
parameters: {
query: {
type: 'string',
description: '검색할 도시 이름이나 주소 (예: "New York", "서울", "1600 Amphitheatre Parkway, Mountain View, CA")'
}
}
},
{
name: 'get-weather',
description: '위도와 경도 좌표, 예보 기간을 입력받아 해당 위치의 현재 날씨와 예보 정보를 제공합니다. Open-Meteo Weather API를 사용합니다.',
parameters: {
latitude: {
type: 'number',
min: -90,
max: 90,
description: '위도 (-90 ~ 90)'
},
longitude: {
type: 'number',
min: -180,
max: 180,
description: '경도 (-180 ~ 180)'
},
forecastDays: {
type: 'number',
min: 1,
max: 16,
optional: true,
default: 7,
description: '예보 기간 (일 단위, 1~16일, 기본값: 7일)'
}
}
},
{
name: 'generate-image',
description: '텍스트 프롬프트를 입력받아 AI 이미지를 생성합니다. Hugging Face FLUX.1-schnell 모델을 사용합니다.',
parameters: {
prompt: {
type: 'string',
description: '생성할 이미지에 대한 설명 (영어로 작성)'
}
}
}
],
resources: [
{
name: 'server-info',
uri: 'server://info',
description: '현재 MCP 서버 정보와 사용 가능한 도구 목록',
mimeType: 'application/json'
}
],
prompts: [
{
name: 'code-review',
description: '코드를 입력받아 미리 정의된 코드 리뷰 프롬프트 템플릿과 결합하여 반환합니다.'
}
]
}
return {
contents: [
{
uri: 'server://info',
mimeType: 'application/json',
text: JSON.stringify(serverInfo, null, 2)
}
]
}
}
)
// 코드 리뷰 프롬프트
server.registerPrompt(
'code-review',
{
title: '코드 리뷰',
description: '코드를 입력받아 미리 정의된 코드 리뷰 프롬프트 템플릿과 결합하여 반환합니다.',
argsSchema: {
code: z.string().describe('리뷰할 코드'),
language: z
.string()
.optional()
.default('auto')
.describe('프로그래밍 언어 (예: typescript, javascript, python, java 등, 기본값: auto)'),
focusAreas: z
.array(z.string())
.optional()
.default([])
.describe('특별히 집중할 리뷰 영역 (예: ["performance", "security", "readability"])')
}
},
async ({
code,
language = 'auto',
focusAreas = []
}: {
code: string
language?: string
focusAreas?: string[]
}) => {
// 미리 정의된 코드 리뷰 프롬프트 템플릿
const codeReviewTemplate = `다음 코드를 리뷰해주세요. 다음 항목들을 중점적으로 검토해주세요:
1. **코드 품질 및 가독성**
- 코드가 명확하고 이해하기 쉬운가요?
- 변수명과 함수명이 의미를 잘 전달하나요?
- 주석이 적절하게 작성되어 있나요?
2. **성능 및 최적화**
- 불필요한 연산이나 중복 코드가 있나요?
- 알고리즘의 시간 복잡도와 공간 복잡도는 적절한가요?
- 메모리 사용이 효율적인가요?
3. **보안 및 에러 처리**
- 보안 취약점이 있나요?
- 에러 처리가 적절하게 구현되어 있나요?
- 입력값 검증이 충분한가요?
4. **아키텍처 및 설계**
- 코드 구조가 적절한가요?
- 관심사의 분리가 잘 되어 있나요?
- 재사용 가능성과 확장성이 좋은가요?
5. **테스트 및 유지보수성**
- 테스트하기 쉬운 구조인가요?
- 유지보수가 용이한가요?
- 문서화가 필요한 부분이 있나요?
${focusAreas.length > 0 ? `\n**특별히 집중할 영역:** ${focusAreas.join(', ')}\n` : ''}
${language !== 'auto' ? `**프로그래밍 언어:** ${language}\n` : ''}
**리뷰할 코드:**
\`\`\`${language !== 'auto' ? language : ''}
${code}
\`\`\`
각 항목에 대해 구체적인 피드백과 개선 제안을 제공해주세요.`
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: codeReviewTemplate
}
}
]
}
}
)
// 이미지 생성 도구
server.registerTool(
'generate-image',
{
description: '텍스트 프롬프트를 입력받아 AI 이미지를 생성합니다. Hugging Face FLUX.1-schnell 모델을 사용합니다.',
inputSchema: z.object({
prompt: z.string().describe('생성할 이미지에 대한 설명 (영어로 작성)')
})
},
async ({ prompt }: { prompt: string }) => {
try {
// Hugging Face API 토큰 확인 (config에서 가져오거나 환경변수에서 가져옴)
const hfToken = config?.HF_TOKEN || process.env.HF_TOKEN
if (!hfToken) {
throw new Error('HF_TOKEN이 설정되지 않았습니다. Hugging Face API 토큰이 필요합니다.')
}
// Hugging Face Inference Client 생성
const client = new InferenceClient(hfToken)
// 이미지 생성
const image: any = 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 image.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const base64Data = buffer.toString('base64')
return {
content: [
{
type: 'image' as const,
data: base64Data,
mimeType: 'image/png'
}
],
_meta: {
annotations: {
audience: ['user'],
priority: 0.9
}
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다'
throw new Error(`이미지 생성 실패: ${errorMessage}`)
}
}
)
// Smithery 배포 형식: server.server 반환
return server.server
}