본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 07. 17:31

실제로 사용되는 MCP 서버를 구축하는 방법

요약

실제 사용 가능한 MCP(Model Context Protocol) 서버를 구축하기 위한 아키텍처와 설계 원칙을 다룹니다. LLM이 도구를 정확히 이해하고 오류를 스스로 복구할 수 있도록 돕는 도구 설명, 에러 메시지, 입력 유효성 검사의 중요성을 강조합니다.

핵심 포인트

  • LLM의 도구 호출을 유도하는 정교한 도구 설명(Tool descriptions) 작성
  • LLM이 스스로 대응할 수 있는 구체적인 에러 메시지 설계
  • 환각 현상을 방지하기 위한 엄격한 입력 유효성 검사
  • 유지보수를 위한 도구별 파일 분리 및 공유 API 클라이언트 패턴

실제로 사용되는 MCP 서버를 구축하는 방법

10개의 서버를 직접 구축해 본 사람의 기술 가이드.

저의 지난 포스트는 배포 문제, 즉 MCP 서버를 구축한다고 해서 반드시 누군가가 사용하게 되는 것은 아니라는 점에 대해 다루었습니다. 이번 포스트는 그 기술적 동반자입니다. 배포가 성공했을 때 서버가 당신을 당황하게 만들지 않도록, 어떻게 하면 서버를 잘 구축할 수 있는지에 대해 다룹니다.

저는 일주일 만에 10개의 MCP 서버를 구축했습니다. 어떤 것들은 훌륭했고, 어떤 것들은 다시 작성하고 싶습니다. 아키텍처(Architecture), 함정(Pitfalls), 그리고 실제로 중요한 패턴들에 대해 제가 배운 점들을 공유합니다.

좋은 MCP 서버의 조건

MCP 서버는 LLM(Large Language Model)과 외부 서비스 사이의 가교 역할을 합니다. LLM이 당신의 도구(Tools)를 호출하면, 당신의 도구가 API를 호출하는 방식입니다. 개념은 간단합니다. 문제는 세부 사항에서 발생합니다.

무엇보다 중요한 세 가지 요소는 다음과 같습니다:

  1. LLM이 실제로 사용할 수 있는 도구 설명 (Tool descriptions). API 문서나 개발자 참조용 문서가 아닙니다. LLM에게 이 도구를 언제 호출해야 하는지, 그리고 호출 시 무엇을 반환하는지를 알려주는 설명이어야 합니다.

  2. LLM의 복구를 돕는 에러 메시지 (Error messages). 도구 실행에 실패하면 에러 메시지가 LLM으로 전달됩니다. 만약 메시지가 "Error 500"이라고만 되어 있다면, LLM은 다음에 무엇을 해야 할지 알 수 없습니다. 반면 "Rate limited — 30초 대기 후 재시도"라고 되어 있다면, LLM은 이를 처리할 수 있습니다.

  3. API에 도달하기 전 실수를 잡아내는 입력 유효성 검사 (Input validation). LLM은 파라미터(Parameters)를 환각(Hallucinate)할 수 있습니다. 서버는 잘못된 입력을 API로 전달하여 모호한 에러를 반환하는 대신, 우아하게 거절해야 합니다.

아키텍처 (The Architecture)

제가 구축한 모든 MCP 서버는 동일한 패턴을 따릅니다:

src/
├── index.ts          # 서버 진입점(Entry point), 도구 등록
├── tools/            # 도구당 하나의 파일 (또는 관련 도구 그룹)
...

도구당 하나의 파일을 사용하는 이유: Resend MCP 서버처럼 도구가 18개나 되는 경우, 이를 모두 하나의 파일에 몰아넣으면 코드베이스를 읽기가 매우 어려워집니다. 도구당 하나의 파일을 사용하면 각 도구를 독립적으로 찾고, 수정하고, 테스트할 수 있습니다.

공유 API 클라이언트가 필요한 이유: 모든 외부 API에는 재시도 로직 (retry logic), 속도 제한 (rate limiting), 그리고 에러 처리 (error handling)가 필요합니다. 한 번 구축하여 어디에서나 사용하세요.

도구 설명 (Tool Descriptions): 당신이 작성할 가장 중요한 코드

아무도 말해주지 않는 사실이 하나 있습니다: 도구 설명 (tool description)은 도구 구현 (tool implementation)보다 더 중요합니다. LLM은 설명을 바탕으로 당신의 도구를 호출할지 여부를 결정합니다. 설명이 모호하면 LLM은 도구를 호출하지 않을 것이고, 너무 구체적이면 LLM이 적절한 상황에서 호출하지 않을 것입니다.

나쁜 설명: ""

description: "Get token price"

좋은 설명: ""

description: "CoinGecko ID를 사용하여 암호화폐 토큰의 현재 가격을 가져옵니다. USD 기준 가격, 시가 총액 (market cap), 24시간 변동률을 반환합니다. 사용자가 특정 토큰의 현재 가격이나 시장 데이터에 대해 물을 때 이 도구를 사용하세요. coin_id 파라미터는 CoinGecko 식별자(예: 'bitcoin', 'ethereum', 'solana')여야 합니다."

좋은 설명은 LLM에게 다음을 알려줍니다:

  • 도구가 무엇을 하는지 (현재 가격 가져오기)
  • 무엇을 반환하는지 (가격, 시가 총액, 24시간 변동률)
  • 언제 사용하는지 (사용자가 현재 가격에 대해 물을 때)
  • 파라미터의 의미가 무엇인지 (심볼이 아닌 CoinGecko ID)

나의 경험 법칙: 설명을 작성할 때, 마치 똑똑한 동료에게 이 함수를 언제 사용해야 하는지 설명한다고 생각하며 작성하세요. API 문서나 변경 로그 (changelog)가 아니라, 사용 가이드 (usage guide)처럼 작성해야 합니다.

LLM을 돕는 에러 처리 (Error Handling)

도구가 실패하면, 에러는 도구 응답 (tool response)으로서 LLM으로 전달됩니다. 그러면 LLM은 재시도할지, 다른 도구를 시도할지, 아니면 사용자에게 문제가 발생했다고 알릴지 결정합니다.

핵심 통찰: 당신의 에러 메시지는 LLM을 향한 프롬프트 (prompt)입니다. 유용하게 만드세요.

// 나쁨
throw new Error("API error");

...
// 나쁨
throw new Error("Invalid input");

...

입력 검증 (Input Validation): 환각된 파라미터 포착하기

LLM은 파라미터를 환각 (hallucinate) 합니다. 문자열이 예상되는 곳에 undefined를 전달하거나, CoinGecko ID가 필요한 곳에 "BTC"를 전달할 수 있습니다. 당신의 서버는 이러한 상황을 우아하게 포착해야 합니다.

// 필수 필드 검증
if (!args.coin_id || typeof args.coin_id !== 'string') {
return {
...

잘못된 입력에 대해 예외(Exception)를 던지지 마세요. 도구 응답(tool response)으로서 도움이 되는 에러 메시지를 반환하세요. LLM은 이를 읽고 올바른 파라미터로 다시 시도할 수 있습니다. 예외를 던지면 MCP 연결이 충돌(crash)할 수 있습니다.

속도 제한 (Rate Limiting): 서버가 차단되지 않도록 주의하세요

모든 API에는 속도 제한 (Rate limits)이 있습니다. 서버가 이를 처리하지 못하면 차단될 것이며, 사용자는 API가 아닌 당신의 서버를 탓할 것입니다.

import PQueue from 'p-queue';

const queue = new PQueue({ 
...

무료 티어의 함정: 대부분의 API 무료 티어는 공격적인 속도 제한을 가지고 있습니다. CoinGecko는 분당 10~30회의 호출을 허용합니다. 만약 LLM이 당신의 도구를 짧은 간격으로 5번 연속 호출한다면, 제한에 걸리게 됩니다. 선제적으로 큐(Queue)를 사용하고 스로틀링 (Throttle)을 적용하세요.

테스트 (Testing): 모두가 건너뛰는 부분

솔직히 말씀드리면, 저도 대부분의 서버에서 테스트를 건너뛰었습니다. 테스트가 없는 서버는 제가 가장 확신하지 못하는 서버들입니다. 여기 최소한의 테스트 설정이 있습니다:

// tests/tools/coingecko.test.ts
import { describe, it, expect } from 'vitest';
import { getCoinPrice } from '../../src/tools/coingecko';
...

테스트해야 할 항목:

  1. 해피 패스 (Happy path) — 유효한 입력에 대해 도구가 예상된 데이터를 반환하는가?
  2. 에러 패스 (Error path) — 도구가 잘못된 입력을 우아하게 처리하는가?
  3. 속도 제한 (Rate limiting) — 도구가 API 제한을 준수하는가?
  4. 엣지 케이스 (Edge cases) — 빈 문자열, null 값, 매우 긴 입력값 등

배포 (Publishing): npm과 GitHub

npm 배포는 간단하지만 주의할 점(gotchas)이 있습니다:

{
  "name": "@supernova123/coingecko-mcp-server",
  "version": "1.0.0",
...

주의사항 1: bin 필드는 당신의 서버를 npx를 통해 실행 가능하게 만드는 요소입니다. 이 필드가 없으면 사용자는 당신의 서버를 실행할 수 없습니다.

주의사항 2: files 필드는 무엇이 배포될지를 제어합니다. 이 필드를 잊어버리면 node_modules가 배포될 수도 있습니다 (제가 어떻게 아는지 묻지는 마세요).

주의사항 3: TypeScript 소스 맵 (source maps). 빌드 설정에서 sourcemap: true를 사용하면, 배포된 패키지가 포함되지 않은 소스 파일을 참조할 수 있습니다. 대신 sourcemap: "inline"을 사용하세요.

GitHub 설정:

  • 설치 안내가 포함된 명확한 README
  • 서버를 어떻게 구성하는지 보여주는 예시 claude_desktop_config.json
  • 설명이 포함된 모든 도구 (tools) 목록
  • 래핑 (wrapping) 중인 API에 대한 링크

Resend MCP 서버: 사례 연구

Resend MCP 서버는 한 세션 만에 도구 수가 10개에서 18개로 늘어났습니다. 제가 무엇을 왜 추가했는지 여기 정리했습니다:

도구 (Tool)이유
send_batch_email사용자는 여러 통의 이메일을 보낼 필요가 있습니다. 한 번에 하나씩 보내는 것은 지루한 작업입니다.
...

교훈: 핵심 사용 사례(이메일 전송, 상태 확인)로 시작한 다음, 관리 도구를 추가하세요. 첫날부터 모든 API 엔드포인트 (endpoint)를 래핑하려고 하지 마세요.

제가 다르게 했을 점

  1. 테스트를 먼저 작성하기. 알고 있습니다, 알고 있어요. 하지만 테스트가 없는 서버는 업데이트를 배포할 때 가장 확신이 서지 않는 대상입니다.

  2. 첫날부터 관찰 가능성 (observability) 추가하기. 도구 호출을 로그로 남기고, 지연 시간 (latency)을 추적하며, 에러 횟수를 기록하세요. 측정할 수 없는 것은 개선할 수 없습니다.

  3. 처음부터 더 나은 설명 작성하기. LLM이 도구를 올바르게 호출하지 않는다는 것을 깨달은 후, 제가 가진 10개의 서버 중 6개의 설명을 다시 작성했습니다.

  4. CLI 테스트 하네스 (test harness) 구축하기. 테스트 파라미터로 각 도구를 호출하고 결과를 출력하는 간단한 스크립트입니다. 수동 테스트보다 훨씬 빠릅니다.

전체 스택 (Full Stack)

제가 모든 MCP 서버에 사용하는 전체 기술 스택은 다음과 같습니다:

Runtime:      Node.js 20+
Language:     TypeScript 5.x
Build:        tsup 또는 tsdown
...

TypeScript를 사용하는 이유: MCP SDK는 TypeScript 네이티브입니다. 공식 SDK, 예제, 그리고 커뮤니티 서버 모두 TypeScript로 되어 있습니다. 생태계와 싸우는 것은 시간을 낭비하는 일입니다.

tsup/tsdown을 사용하는 이유: 빌드가 빠르며, TypeScript 컴파일과 번들링 (bundling)을 한 단계로 처리합니다. 출력물은 어디서나 실행 가능한 깔끔한 JavaScript입니다.

마무리

MCP 서버를 구축하는 것은 쉬운 부분입니다. 하지만 명확한 설명(descriptions), 유용한 에러 메시지(errors), 그리고 견고한 검증(validation)을 갖추어 LLM이 실제로 사용하고 싶어 하는 서버를 만드는 것이 진짜 어려운 부분입니다.

배포 문제(distribution problem)는 실재하지만(저의 이전 포스트를 참조하세요), 이는 서버가 사용할 가치가 있을 때만 의미가 있습니다. 먼저 서버를 잘 만드는 데 집중한 다음, 배포 방법을 고민하세요.

만약 MCP 서버를 구축하고 계신다면, 여러분이 무엇을 작업하고 있는지 정말 듣고 싶습니다. 댓글을 남겨주시거나 GitHub에서 저를 찾아주세요: @supernova123.

이 글은 저의 MCP 서버 시리즈 중 파트 2입니다. 파트 1에서는 배포 문제를 다루었습니다: I Built 10 MCP Servers in a Week. Here's What Nobody Tells You About Distribution.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0