import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
import { InferenceClient } from '@huggingface/inference'
// Configuration schema for Smithery
export const configSchema = z.object({
HF_TOKEN: z.string().optional().describe('Hugging Face API token for image generation')
})
// Blob을 base64로 변환하는 헬퍼 함수
async function blobToBase64(blob: Blob): Promise<string> {
const arrayBuffer = await blob.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
return buffer.toString('base64')
}
// Required: Export default createServer function
export default function createServer({ config }: { config: z.infer<typeof configSchema> }) {
// Create server instance
const server = new McpServer({
name: 'hello-mcp',
version: '1.0.0'
})
// Hugging Face Inference Client - lazy initialization
let hfClient: InferenceClient | null = null
const getHfClient = () => {
if (!config.HF_TOKEN) {
throw new Error('HF_TOKEN이 설정되지 않았습니다. 이미지 생성을 사용하려면 HF_TOKEN을 설정해주세요.')
}
if (!hfClient) {
hfClient = new InferenceClient(config.HF_TOKEN)
}
return hfClient
}
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
switch (operator) {
case '+':
result = num1 + num2
break
case '-':
result = num1 - num2
break
case '*':
result = num1 * num2
break
case '/':
if (num2 === 0) {
throw new Error('0으로 나눌 수 없습니다.')
}
result = num1 / num2
break
default:
throw new Error(`지원하지 않는 연산자: ${operator}`)
}
const resultText = `${num1} ${operator} ${num2} = ${result}`
return {
content: [
{
type: 'text' as const,
text: resultText
}
],
structuredContent: {
content: [
{
type: 'text' as const,
text: resultText
}
]
}
}
}
)
server.registerTool(
'time',
{
description: 'timezone 기반으로 현재 시간을 알려줍니다.',
inputSchema: z.object({
timezone: z
.string()
.describe('IANA timezone 이름 (예: Asia/Seoul, America/New_York, Europe/London)')
}),
outputSchema: z.object({
content: z
.array(
z.object({
type: z.literal('text'),
text: z.string().describe('현재 시간')
})
)
.describe('현재 시간')
})
},
async ({ timezone }) => {
try {
const now = new Date()
const formatter = new Intl.DateTimeFormat('ko-KR', {
timeZone: 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 resultText = `${timezone}의 현재 시간: ${formattedTime}`
return {
content: [
{
type: 'text' as const,
text: resultText
}
],
structuredContent: {
content: [
{
type: 'text' as const,
text: resultText
}
]
}
}
} catch (error) {
throw new Error(`유효하지 않은 timezone입니다: ${timezone}`)
}
}
)
server.registerTool(
'geocode',
{
description: '도시 이름이나 주소를 입력받아서 위도와 경도 좌표를 반환합니다.',
inputSchema: z.object({
query: z
.string()
.describe('검색할 도시 이름이나 주소 (예: "Seoul", "New York", "서울시 강남구")')
}),
outputSchema: z.object({
content: z
.array(
z.object({
type: z.literal('text'),
text: z.string().describe('위도와 경도 좌표 정보')
})
)
.describe('위도와 경도 좌표')
})
},
async ({ query }) => {
try {
const encodedQuery = encodeURIComponent(query)
const url = `https://nominatim.openstreetmap.org/search?q=${encodedQuery}&format=json&limit=1&addressdetails=1`
const response = await fetch(url, {
headers: {
'User-Agent': 'hello-mcp/1.0.0'
}
})
if (!response.ok) {
throw new Error(`Nominatim 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}\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(`Geocoding 오류: ${errorMessage}`)
}
}
)
server.registerTool(
'get-weather',
{
description: '위도와 경도 좌표, 예보 기간을 입력받아서 해당 위치의 현재 날씨와 예보 정보를 제공합니다.',
inputSchema: z.object({
latitude: z.number().describe('위도 좌표'),
longitude: z.number().describe('경도 좌표'),
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 {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t_weather=true&hourly=temperature_2m,relative_humidity_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(`Open-Meteo API 요청 실패: ${response.status} ${response.statusText}`)
}
const data = await response.json()
// 현재 날씨 정보
const current = data.current_weather
const currentTemp = current.temperature
const currentWeatherCode = current.weathercode
const currentWindSpeed = current.windspeed
// 날씨 코드를 설명으로 변환하는 함수
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}`
}
// 일별 예보 정보
const daily = data.daily
const dailyForecasts: string[] = []
for (let i = 0; i < Math.min(forecastDays, daily.time.length); i++) {
const date = daily.time[i]
const maxTemp = daily.temperature_2m_max[i]
const minTemp = daily.temperature_2m_min[i]
const precipitation = daily.precipitation_sum[i]
const weatherCode = daily.weathercode[i]
dailyForecasts.push(
`${date}: 최고 ${maxTemp}°C / 최저 ${minTemp}°C, 강수량 ${precipitation}mm, ${getWeatherDescription(weatherCode)}`
)
}
const resultText = `📍 위치: (${latitude}, ${longitude})
🌡️ 현재 날씨:
- 온도: ${currentTemp}°C
- 날씨: ${getWeatherDescription(currentWeatherCode)}
- 풍속: ${currentWindSpeed} km/h
📅 ${forecastDays}일 예보:
${dailyForecasts.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.registerTool(
'generate-image',
{
description: '텍스트 프롬프트를 입력받아서 AI로 이미지를 생성합니다.',
inputSchema: z.object({
prompt: z.string().describe('이미지 생성을 위한 텍스트 프롬프트')
}),
outputSchema: z.object({
content: z
.array(
z.object({
type: z.literal('image'),
data: z.string().describe('base64로 인코딩된 이미지 데이터'),
mimeType: z.string().describe('이미지 MIME 타입'),
annotations: z
.object({
audience: z.array(z.string()).describe('대상 사용자'),
priority: z.number().describe('우선순위')
})
.optional()
})
)
.describe('생성된 이미지')
})
},
async ({ prompt }) => {
try {
const client = getHfClient()
const imageResult: any = await client.textToImage({
provider: 'auto',
model: 'black-forest-labs/FLUX.1-schnell',
inputs: prompt,
parameters: { num_inference_steps: 5 }
})
// textToImage는 Blob을 반환하지만 타입 정의가 다를 수 있으므로 처리
let imageBlob: Blob
if (imageResult instanceof Blob) {
imageBlob = imageResult
} else if (typeof imageResult === 'string') {
// base64 문자열인 경우
const binaryString = Buffer.from(imageResult, 'base64').toString('binary')
const bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
imageBlob = new Blob([bytes], { type: 'image/png' })
} else if (imageResult && typeof imageResult.blob === 'function') {
// Response 객체인 경우
imageBlob = await imageResult.blob()
} else {
throw new Error('예상치 못한 이미지 반환 형식입니다.')
}
const base64Data = await blobToBase64(imageBlob)
return {
content: [
{
type: 'image' as const,
data: base64Data,
mimeType: 'image/png',
annotations: {
audience: ['user'],
priority: 0.9
}
}
],
structuredContent: {
content: [
{
type: 'image' as const,
data: base64Data,
mimeType: 'image/png',
annotations: {
audience: ['user'],
priority: 0.9
}
}
]
}
}
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: '알 수 없는 오류가 발생했습니다.'
throw new Error(`이미지 생성 오류: ${errorMessage}`)
}
}
)
// 서버 정보와 도구 목록을 저장
const serverInfo = {
name: 'hello-mcp',
version: '1.0.0',
tools: [
{
name: 'greet',
description: '이름과 언어를 입력하면 인사말을 반환합니다.'
},
{
name: 'calculator',
description: '두 개의 숫자와 연산자를 입력받아 사칙연산 결과를 반환합니다.'
},
{
name: 'time',
description: 'timezone 기반으로 현재 시간을 알려줍니다.'
},
{
name: 'geocode',
description: '도시 이름이나 주소를 입력받아서 위도와 경도 좌표를 반환합니다.'
},
{
name: 'get-weather',
description: '위도와 경도 좌표, 예보 기간을 입력받아서 해당 위치의 현재 날씨와 예보 정보를 제공합니다.'
},
{
name: 'generate-image',
description: '텍스트 프롬프트를 입력받아서 AI로 이미지를 생성합니다.'
}
]
}
// 코드 리뷰 프롬프트 템플릿
const codeReviewTemplate = `다음 코드를 리뷰해주세요. 다음 항목들을 포함하여 분석해주세요:
1. **코드 품질**
- 가독성: 코드가 명확하고 이해하기 쉬운가요?
- 구조: 코드 구조가 논리적이고 체계적인가요?
- 네이밍: 변수, 함수, 클래스 이름이 의미를 잘 전달하나요?
2. **성능**
- 알고리즘 효율성: 시간 복잡도와 공간 복잡도가 적절한가요?
- 최적화 가능성: 성능을 개선할 수 있는 부분이 있나요?
3. **보안**
- 입력 검증: 사용자 입력이 적절히 검증되나요?
- 취약점: 보안 취약점이 있나요?
- 데이터 보호: 민감한 데이터가 안전하게 처리되나요?
4. **에러 처리**
- 예외 처리: 예외 상황이 적절히 처리되나요?
- 에러 메시지: 에러 메시지가 명확한가요?
5. **개선 사항**
- 리팩토링 제안: 코드를 개선할 수 있는 방법은 무엇인가요?
- Best Practices: 해당 언어/프레임워크의 모범 사례를 따르고 있나요?
6. **테스트**
- 테스트 가능성: 코드가 테스트하기 쉬운 구조인가요?
- 엣지 케이스: 엣지 케이스가 고려되었나요?
코드:
\`\`\`
{code}
\`\`\`
위 항목들을 바탕으로 상세한 코드 리뷰를 작성해주세요.`
// 코드 리뷰 프롬프트 등록
server.registerPrompt(
'code-review',
{
description: '코드를 입력받아서 미리 정의된 코드 리뷰 프롬프트 템플릿과 결합하여 반환합니다.',
argsSchema: {
code: z.string().describe('리뷰할 코드')
}
},
async (args) => {
const { code } = args as { code: string }
const prompt = codeReviewTemplate.replace('{code}', code)
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: prompt
}
}
]
}
}
)
// 서버 정보 리소스 등록
server.registerResource(
'server-info',
'hello-mcp://server-info',
{
description: '현재 서버 정보와 사용 가능한 도구 목록',
mimeType: 'application/json'
},
async () => {
const info = {
server: {
name: serverInfo.name,
version: serverInfo.version,
uptime: process.uptime(),
timestamp: new Date().toISOString()
},
tools: serverInfo.tools
}
return {
contents: [
{
uri: 'hello-mcp://server-info',
mimeType: 'application/json',
text: JSON.stringify(info, null, 2)
}
]
}
}
)
// Must return the MCP server object
return server.server
}