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> } = { config: {} }) {
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: z.object({
a: z.number().describe('첫 번째 숫자'),
b: 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 ({ 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) {
return {
content: [
{
type: 'text' as const,
text: '오류: 0으로 나눌 수 없습니다.'
}
]
}
}
result = a / b
break
}
const resultText = `${a} ${operator} ${b} = ${result}`
return {
content: [
{
type: 'text' as const,
text: resultText
}
],
structuredContent: {
content: [
{
type: 'text' as const,
text: resultText
}
]
}
}
}
)
// 지오코딩 도구
server.registerTool(
'geocode',
{
description: '도시 이름이나 주소를 입력받아서 위도와 경도 좌표를 반환합니다.',
inputSchema: z.object({
address: z.string().describe('도시 이름이나 주소')
}),
outputSchema: z.object({
content: z
.array(
z.object({
type: z.literal('text'),
text: z.string().describe('위도와 경도 좌표')
})
)
.describe('위도와 경도 좌표')
})
},
async ({ address }) => {
try {
const url = new URL('https://nominatim.openstreetmap.org/search')
url.searchParams.set('q', address)
url.searchParams.set('format', 'json')
url.searchParams.set('addressdetails', '1')
url.searchParams.set('limit', '1')
const response = await fetch(url.toString(), {
headers: {
'User-Agent': 'MCP-Server/1.0 (geocoding-tool)'
}
})
if (!response.ok) {
throw new Error(`Nominatim API error: ${response.status}`)
}
const data = await response.json()
if (!data || data.length === 0) {
return {
content: [
{
type: 'text' as const,
text: `주소를 찾을 수 없습니다: ${address}`
}
]
}
}
const result = data[0]
const lat = parseFloat(result.lat)
const lon = parseFloat(result.lon)
const displayName = result.display_name || address
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}`
}
]
}
}
}
)
// 날씨 도구
server.registerTool(
'get-weather',
{
description: '위도와 경도 좌표, 예보 기간을 입력받아서 해당 위치의 현재 날씨와 예보 정보를 제공합니다.',
inputSchema: z.object({
latitude: z.number().describe('위도 좌표'),
longitude: z.number().describe('경도 좌표'),
forecast_days: z
.number()
.int()
.min(1)
.max(16)
.optional()
.default(7)
.describe('예보 기간 (일) - 기본값: 7, 최대: 16')
}),
outputSchema: z.object({
content: z
.array(
z.object({
type: z.literal('text'),
text: z.string().describe('날씨 정보')
})
)
.describe('날씨 정보')
})
},
async ({ latitude, longitude, forecast_days = 7 }) => {
try {
const url = new URL('https://api.open-meteo.com/v1/forecast')
url.searchParams.set('latitude', latitude.toString())
url.searchParams.set('longitude', longitude.toString())
url.searchParams.set('forecast_days', forecast_days.toString())
url.searchParams.set(
'hourly',
'temperature_2m,relative_humidity_2m,precipitation,weather_code,wind_speed_10m'
)
url.searchParams.set(
'daily',
'temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code'
)
url.searchParams.set('timezone', 'auto')
const response = await fetch(url.toString())
if (!response.ok) {
throw new Error(`Open-Meteo API error: ${response.status}`)
}
const data = await response.json()
if (data.error) {
return {
content: [
{
type: 'text' as const,
text: `날씨 정보 오류: ${data.reason || '알 수 없는 오류'}`
}
]
}
}
// 현재 날씨 정보 (첫 번째 hourly 데이터)
const currentHourly = data.hourly
const currentTime = currentHourly?.time?.[0]
const currentTemp = currentHourly?.temperature_2m?.[0]
const currentHumidity = currentHourly?.relative_humidity_2m?.[0]
const currentPrecipitation = currentHourly?.precipitation?.[0]
const currentWeatherCode = currentHourly?.weather_code?.[0]
const currentWindSpeed = currentHourly?.wind_speed_10m?.[0]
// 일일 예보 정보
const daily = data.daily
const dailyTimes = daily?.time || []
const dailyMaxTemps = daily?.temperature_2m_max || []
const dailyMinTemps = daily?.temperature_2m_min || []
const dailyPrecipitation = daily?.precipitation_sum || []
const dailyWeatherCodes = daily?.weather_code || []
// 결과 텍스트 생성
let resultText = `📍 위치: 위도 ${latitude}, 경도 ${longitude}\n`
resultText += `⏰ 시간대: ${data.timezone || 'N/A'}\n\n`
// 현재 날씨
resultText += `🌤️ 현재 날씨\n`
resultText += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`
if (currentTime) {
resultText += `시간: ${currentTime}\n`
}
if (currentTemp !== undefined) {
resultText += `온도: ${currentTemp}°C\n`
}
if (currentHumidity !== undefined) {
resultText += `습도: ${currentHumidity}%\n`
}
if (currentPrecipitation !== undefined) {
resultText += `강수량: ${currentPrecipitation}mm\n`
}
if (currentWindSpeed !== undefined) {
resultText += `풍속: ${currentWindSpeed}km/h\n`
}
resultText += `\n`
// 일일 예보
resultText += `📅 ${forecast_days}일 예보\n`
resultText += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`
for (let i = 0; i < Math.min(dailyTimes.length, forecast_days); i++) {
const date = dailyTimes[i]
const maxTemp = dailyMaxTemps[i]
const minTemp = dailyMinTemps[i]
const precip = dailyPrecipitation[i]
const weatherCode = dailyWeatherCodes[i]
resultText += `\n📆 ${date}\n`
if (maxTemp !== undefined && minTemp !== undefined) {
resultText += ` 최고: ${maxTemp}°C / 최저: ${minTemp}°C\n`
}
if (precip !== undefined) {
resultText += ` 강수량: ${precip}mm\n`
}
if (weatherCode !== undefined) {
resultText += ` 날씨 코드: ${weatherCode}\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}`
}
]
}
}
}
)
// Hugging Face 이미지 생성 도구
server.registerTool(
'generate-image',
{
description: 'Hugging Face FLUX.1 모델을 사용하여 텍스트 프롬프트로 이미지를 생성합니다.',
inputSchema: z.object({
prompt: z.string().describe('이미지를 생성할 텍스트 프롬프트')
})
},
async ({ prompt }) => {
try {
const hfToken = config?.hfToken || process.env.HF_TOKEN
if (!hfToken) {
return {
content: [
{
type: 'text' as const,
text: '오류: HF_TOKEN이 설정되지 않았습니다. configSchema에서 hfToken을 설정하거나 HF_TOKEN 환경 변수를 설정해 주세요.'
}
]
}
}
const client = new InferenceClient(hfToken)
const image = await client.textToImage(
{
provider: 'auto',
model: 'black-forest-labs/FLUX.1-schnell',
inputs: prompt,
parameters: { num_inference_steps: 5 }
},
{ outputType: 'blob' }
)
// Blob을 Base64로 변환
const arrayBuffer = await (image as Blob).arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const base64Data = buffer.toString('base64')
return {
content: [
{
type: 'image' as const,
data: base64Data,
mimeType: 'image/png'
}
]
}
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: '알 수 없는 오류가 발생했습니다.'
return {
content: [
{
type: 'text' as const,
text: `이미지 생성 오류: ${errorMessage}`
}
]
}
}
}
)
// 서버 정보 및 도구 목록 리소스
server.resource(
'server-info',
'mcp://server-info',
{
description: '현재 서버 정보와 사용 가능한 도구 목록',
mimeType: 'application/json'
},
async () => {
const serverInfo = {
server: {
name: serverName,
version: serverVersion,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
nodeVersion: process.version,
platform: process.platform
},
tools: [
{
name: 'greet',
description: '이름과 언어를 입력하면 인사말을 반환합니다.',
parameters: {
name: {
type: 'string',
description: '인사할 사람의 이름',
required: true
},
language: {
type: 'enum',
values: ['ko', 'en'],
description: '인사 언어 (기본값: en)',
required: false,
default: 'en'
}
}
},
{
name: 'calculator',
description: '두 개의 숫자와 연산자를 입력받아 사칙연산 결과를 반환합니다.',
parameters: {
a: {
type: 'number',
description: '첫 번째 숫자',
required: true
},
b: {
type: 'number',
description: '두 번째 숫자',
required: true
},
operator: {
type: 'enum',
values: ['+', '-', '*', '/'],
description: '연산자 (+, -, *, /)',
required: true
}
}
},
{
name: 'geocode',
description: '도시 이름이나 주소를 입력받아서 위도와 경도 좌표를 반환합니다.',
parameters: {
address: {
type: 'string',
description: '도시 이름이나 주소',
required: true
}
}
},
{
name: 'get-weather',
description: '위도와 경도 좌표, 예보 기간을 입력받아서 해당 위치의 현재 날씨와 예보 정보를 제공합니다.',
parameters: {
latitude: {
type: 'number',
description: '위도 좌표',
required: true
},
longitude: {
type: 'number',
description: '경도 좌표',
required: true
},
forecast_days: {
type: 'number',
description: '예보 기간 (일) - 기본값: 7, 최대: 16',
required: false,
default: 7,
min: 1,
max: 16
}
}
},
{
name: 'generate-image',
description: 'Hugging Face FLUX.1 모델을 사용하여 텍스트 프롬프트로 이미지를 생성합니다.',
parameters: {
prompt: {
type: 'string',
description: '이미지를 생성할 텍스트 프롬프트',
required: true
}
}
}
]
}
return {
contents: [
{
uri: 'mcp://server-info',
mimeType: 'application/json',
text: JSON.stringify(serverInfo, null, 2)
}
]
}
}
)
// 코드 리뷰 프롬프트 템플릿
const codeReviewPromptTemplate = `다음 코드를 리뷰해 주세요. 다음 항목들을 중점적으로 검토해 주세요:
## 리뷰 체크리스트
### 1. 기능성 (Functionality)
- 코드가 의도한 기능을 올바르게 구현하는가?
- 엣지 케이스와 예외 상황을 적절히 처리하는가?
- 입력 검증이 충분한가?
### 2. 코드 품질 (Code Quality)
- 코드가 읽기 쉽고 이해하기 쉬운가?
- 변수와 함수 이름이 명확한가?
- 중복 코드가 있는가?
- 적절한 주석이 있는가?
### 3. 성능 (Performance)
- 불필요한 연산이나 반복이 있는가?
- 메모리 사용이 효율적인가?
- 알고리즘의 시간 복잡도가 적절한가?
### 4. 보안 (Security)
- 보안 취약점이 있는가?
- 입력 데이터를 적절히 검증하는가?
- 민감한 정보가 노출되지 않는가?
### 5. 유지보수성 (Maintainability)
- 코드 구조가 모듈화되어 있는가?
- 테스트하기 쉬운 구조인가?
- 확장 가능한 구조인가?
### 6. 베스트 프랙티스 (Best Practices)
- 언어/프레임워크의 베스트 프랙티스를 따르는가?
- 코딩 컨벤션을 준수하는가?
## 리뷰할 코드
\`\`\`{language}
{code}
\`\`\`
위 코드에 대한 상세한 리뷰를 제공해 주세요. 발견된 문제점, 개선 사항, 그리고 긍정적인 부분을 모두 포함해 주세요.`
// 코드 리뷰 프롬프트 등록
server.registerPrompt(
'code-review',
{
description: '코드를 입력받아 코드 리뷰를 위한 프롬프트를 생성합니다.',
argsSchema: {
code: z.string().describe('리뷰할 코드'),
language: z
.string()
.optional()
.default('')
.describe('프로그래밍 언어 (예: typescript, javascript, python 등)')
}
},
async (args) => {
const { code, language } = args
// 언어가 지정되지 않은 경우 자동 감지 시도
let detectedLanguage = language || ''
if (!detectedLanguage) {
// 간단한 언어 감지 로직
if (code.includes('function') && code.includes('const') && code.includes('=>')) {
detectedLanguage = 'javascript'
} else if (code.includes('def ') || code.includes('import ') && code.includes('print(')) {
detectedLanguage = 'python'
} else if (code.includes('interface') || code.includes('type ') || code.includes(': ')) {
detectedLanguage = 'typescript'
} else if (code.includes('class ') && code.includes('public ')) {
detectedLanguage = 'java'
} else {
detectedLanguage = 'text'
}
}
// 프롬프트 템플릿에 코드와 언어 삽입
const prompt = codeReviewPromptTemplate
.replace('{code}', code)
.replace('{language}', detectedLanguage)
return {
messages: [
{
role: 'user' as const,
content: {
type: 'text' as const,
text: prompt
}
}
]
}
}
)
// Smithery는 server.server 객체를 반환해야 함
return server.server
}