import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { InferenceClient } from '@huggingface/inference'
import { z } from 'zod'
// 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: '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
let operationSymbol: string
switch (operator) {
case '+':
result = a + b
operationSymbol = '+'
break
case '-':
result = a - b
operationSymbol = '-'
break
case '*':
result = a * b
operationSymbol = '×'
break
case '/':
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(
'get_time',
{
description:
'타임존을 입력받아 해당 타임존의 현재 시간을 반환합니다.',
inputSchema: z.object({
timezone: z
.string()
.describe(
'IANA 타임존 이름 (예: 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 timeText = `${timezone}의 현재 시간: ${formattedTime}`
return {
content: [
{
type: 'text' as const,
text: timeText
}
],
structuredContent: {
content: [
{
type: 'text' as const,
text: timeText
}
]
}
}
} catch (error) {
throw new Error(
`유효하지 않은 타임존입니다: ${timezone}. 올바른 IANA 타임존 이름을 입력해주세요. (예: Asia/Seoul, America/New_York)`
)
}
}
)
server.registerTool(
'geocode',
{
description:
'도시 이름이나 주소를 입력받아서 위도와 경도 좌표를 반환합니다.',
inputSchema: z.object({
query: z
.string()
.describe(
'검색할 도시 이름이나 주소 (예: 서울, New York, 1600 Amphitheatre Parkway, Mountain View)'
)
}),
outputSchema: z.object({
content: z
.array(
z.object({
type: z.literal('text'),
text: z.string().describe('위도와 경도 좌표 정보')
})
)
.describe('위도와 경도 좌표 정보')
})
},
async ({ query }) => {
try {
// Nominatim API 엔드포인트
const baseUrl = 'https://nominatim.openstreetmap.org/search'
const params = new URLSearchParams({
q: query,
format: 'json',
limit: '1',
addressdetails: '1'
})
const url = `${baseUrl}?${params.toString()}`
const response = await fetch(url, {
headers: {
'User-Agent': 'MCP-Server/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) {
if (error instanceof Error) {
throw new Error(`지오코딩 오류: ${error.message}`)
}
throw new Error('지오코딩 중 알 수 없는 오류가 발생했습니다.')
}
}
)
server.registerTool(
'code-review',
{
description:
'코드를 입력받아서 미리 정의된 코드 리뷰 프롬프트 템플릿과 결합하여 반환합니다.',
inputSchema: z.object({
code: z
.string()
.describe('리뷰할 코드 (전체 코드 또는 코드 스니펫)'),
language: z
.string()
.optional()
.describe('프로그래밍 언어 (예: TypeScript, JavaScript, Python, Java 등)'),
focus: z
.string()
.optional()
.describe('특별히 집중할 리뷰 영역 (예: 성능, 보안, 가독성, 테스트 등)')
}),
outputSchema: z.object({
content: z
.array(
z.object({
type: z.literal('text'),
text: z.string().describe('코드 리뷰 프롬프트')
})
)
.describe('코드 리뷰 프롬프트')
})
},
async ({ code, language, focus }) => {
// 미리 정의된 코드 리뷰 프롬프트 템플릿
const reviewTemplate = `다음 코드를 리뷰해주세요. 다음 항목들을 중점적으로 검토해주세요:
## 리뷰 체크리스트
### 1. 코드 품질
- [ ] 코드 가독성과 명확성
- [ ] 네이밍 컨벤션 준수
- [ ] 코드 구조와 조직화
- [ ] 중복 코드 제거
### 2. 기능성
- [ ] 로직의 정확성
- [ ] 엣지 케이스 처리
- [ ] 에러 핸들링
- [ ] 입력 검증
### 3. 성능
- [ ] 알고리즘 효율성
- [ ] 불필요한 연산 최소화
- [ ] 메모리 사용 최적화
- [ ] 데이터베이스 쿼리 최적화 (해당되는 경우)
### 4. 보안
- [ ] 보안 취약점
- [ ] 입력 검증 및 sanitization
- [ ] 인증/인가 처리
- [ ] 민감한 정보 노출 방지
### 5. 유지보수성
- [ ] 코드 주석 및 문서화
- [ ] 테스트 가능성
- [ ] 확장성 고려
- [ ] 의존성 관리
### 6. 모범 사례
- [ ] 언어별 모범 사례 준수
- [ ] 디자인 패턴 적용
- [ ] SOLID 원칙 준수
- [ ] DRY (Don't Repeat Yourself) 원칙
## 리뷰 요청 코드
\`\`\`${language || 'code'}
${code}
\`\`\`
${focus ? `## 특별 집중 영역\n\n${focus}에 대해 특히 자세히 검토해주세요.\n\n` : ''}## 리뷰 형식
다음 형식으로 리뷰를 작성해주세요:
### 👍 잘된 점
- 구체적인 칭찬 사항
### 🔧 개선 사항
- 구체적인 개선 제안과 이유
### ⚠️ 잠재적 문제
- 발견된 문제점과 해결 방안
### 💡 제안
- 추가 개선 아이디어나 대안
각 항목에 대해 구체적인 예시와 함께 설명해주세요.`
return {
content: [
{
type: 'text' as const,
text: reviewTemplate
}
],
structuredContent: {
content: [
{
type: 'text' as const,
text: reviewTemplate
}
]
}
}
}
)
server.registerTool(
'generate-image',
{
description:
'텍스트 프롬프트를 입력받아 AI로 이미지를 생성하고 base64 인코딩된 이미지를 반환합니다.',
inputSchema: z.object({
prompt: z
.string()
.describe('생성할 이미지에 대한 설명 프롬프트 (예: Astronaut riding a horse)')
})
},
async ({ prompt }) => {
try {
const client = new InferenceClient(config.hfToken)
const image = await client.textToImage({
provider: 'auto',
model: 'black-forest-labs/FLUX.1-schnell',
inputs: prompt,
parameters: { num_inference_steps: 4 }
})
// Blob을 base64로 변환
const imageBlob = image as unknown as Blob
const arrayBuffer = await imageBlob.arrayBuffer()
const base64Data = Buffer.from(arrayBuffer).toString('base64')
return {
content: [
{
type: 'image' as const,
data: base64Data,
mimeType: 'image/png'
}
]
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`이미지 생성 오류: ${error.message}`)
}
throw new Error('이미지 생성 중 알 수 없는 오류가 발생했습니다.')
}
}
)
// 서버 정보 및 도구 목록을 반환하는 Resource 등록
server.registerResource(
'server-info',
'mcp://server-info',
{
description: '현재 서버 정보와 사용 가능한 도구 목록',
mimeType: 'application/json'
},
async () => {
// 등록된 도구 목록 수집
const tools = [
{
name: 'greet',
description: '이름과 언어를 입력하면 인사말을 반환합니다.'
},
{
name: 'calculator',
description:
'두 개의 숫자와 연산자를 입력받아 사칙연산을 수행하고 결과를 반환합니다.'
},
{
name: 'get_time',
description:
'타임존을 입력받아 해당 타임존의 현재 시간을 반환합니다.'
},
{
name: 'geocode',
description:
'도시 이름이나 주소를 입력받아서 위도와 경도 좌표를 반환합니다.'
},
{
name: 'code-review',
description:
'코드를 입력받아서 미리 정의된 코드 리뷰 프롬프트 템플릿과 결합하여 반환합니다.'
},
{
name: 'generate-image',
description:
'텍스트 프롬프트를 입력받아 AI로 이미지를 생성하고 base64 인코딩된 이미지를 반환합니다.'
}
]
const serverInfo = {
server: {
name: 'my-mcp-server',
version: '1.0.0',
timestamp: new Date().toISOString(),
uptime: process.uptime()
},
tools: tools,
totalTools: tools.length
}
return {
contents: [
{
uri: 'mcp://server-info',
mimeType: 'application/json',
text: JSON.stringify(serverInfo, null, 2)
}
]
}
}
)
// Smithery 배포를 위해 MCP 서버 객체 반환
return server.server
}