TypeScript와 Zod를 사용하여 첫 번째 Model Context Protocol (MCP) 서버 구축하기
요약
Anthropic이 개발한 Model Context Protocol(MCP)을 사용하여 TypeScript와 Zod로 MCP 서버를 구축하는 가이드입니다. MCP의 클라이언트-서버 아키텍처를 통해 AI 모델과 외부 데이터 소스 간의 표준화된 연결 방식을 설명합니다.
핵심 포인트
- MCP는 AI 모델과 외부 도구를 연결하는 개방형 범용 표준입니다.
- 클라이언트-서버 아키텍처를 통해 마이크로서비스와 유사한 구조를 가집니다.
- TypeScript와 Zod를 활용하여 타입 안정성이 높은 서버 구축이 가능합니다.
- 파편화된 커스텀 통합 방식의 문제를 해결하고 표준화된 인터페이스를 제공합니다.
최근 AI 에이전트를 구축하거나 대규모 언어 모델 (LLM)을 다루어 보았다면, 통합의 벽에 부딪혔을 가능성이 높습니다. 역사적으로 Claude나 GPT-4와 같은 LLM을 운영 데이터베이스 쿼리, 시스템 로그 확인, 또는 로컬 파일 시스템과의 상호작용과 같은 외부 환경에 연결하려면, 임시적이고 취약한 통합 계층을 구축해야 했습니다.
모든 AI 프레임워크는 기본적인 오류를 처리하기 위해서조차 맞춤형 도구 정의 (bespoke tool definitions), 커스텀 JSON 파싱 루프, 그리고 하드코딩된 프롬프트 엔지니어링 (prompt engineering)을 요구했습니다. 이러한 커스텀 통합을 구축하는 것은 마치 HTTP 표준화 이전의 초기 웹 개발 시절과 매우 흡사했는데, 당시에는 모든 브라우저 벤더가 독자적인 렌더링 엔진을 구현하여 개발자들이 파편화된 코드베이스를 유지 관리해야만 했습니다.
이때 등장한 것이 바로 **Model Context Protocol (MCP)**입니다. Anthropic에서 개발한 MCP는 AI 모델을 데이터 소스 및 도구에 연결하기 위한 개방형의 범용 표준을 수립합니다.
이 종합 가이드에서는 MCP의 아키텍처 기초를 깊이 있게 파고들고, 이것이 마이크로서비스 (microservices)와 같은 현대적인 웹 개발 패러다임에 어떻게 깔끔하게 매핑되는지 탐구하며, TypeScript, 공식 @modelcontextprotocol/sdk, 그리고 엄격한 런타임 입력 검증을 위한 Zod를 사용하여 처음부터 프로덕션급의 독립형 MCP 서버를 구축해 보겠습니다.
왜 MCP인가? 아키텍처 패러다임의 전환
TypeScript로 MCP 서버를 구축하는 것이 왜 AI 애플리케이션 개발의 게임 체인저인지 진정으로 이해하려면, LLM이 역사적으로 외부 환경과 어떻게 상호작용했는지를 살펴보아야 합니다.
전통적인 설정에서는 AI 모델이 데이터를 가져와야 할 때, 개발자들은 API를 경직된 프롬프트 지침으로 감싸는 커스텀 코드를 작성합니다. 하지만 모델은 확률적입니다. 모델은 토큰 단위로 텍스트를 생성하며, 이는 데이터 타입에 관한 미묘한 환각 (hallucinations), 필수 인자 누락, 또는 JSON 구조의 잘못된 형식 지정 등에 취약함을 의미합니다.
MCP는 현대적인 분산 시스템 (distributed systems)의 관점에서 AI 아키텍처를 바라봄으로써 이러한 파편화 문제를 해결하며, **Model Context Protocol 클라이언트-서버 아키텍처 (Client-Server Architecture)**를 고전적인 **REST 및 gRPC를 통한 마이크로서비스 아키텍처 (Microservices Architecture)**에 직접 매핑합니다.
- LLM 호스트 (API 게이트웨이): Claude Desktop, IDE 확장 프로그램 또는 커스텀 에이전트 런타임 (agent runtimes)과 같은 애플리케이션이 호스트 환경 역할을 합니다. 호스트는 사용자 인터페이스를 관리하고, 대화 문맥 (conversational context)을 유지하며, LLM 자체를 호스팅합니다. 하지만 호스트는 의도적으로 사용자의 로컬 머신 내부 데이터베이스, 독점적 파일 시스템 또는 특화된 엔터프라이즈 도구에 대한 직접적인 접근 권한을 갖지 않습니다.
- MCP 서버 (마이크로서비스): 특정 도구 (tools), 리소스 (resources), 그리고 프롬프트 템플릿 (prompt templates)을 캡슐화하는 독립적이고 자급자족적인 프로세스로, 종종 TypeScript로 작성됩니다. 이 서버는 더 넓은 대화 기록이나 사용자의 전반적인 의도에 대해서는 전혀 알지 못하며, 오직 엄격하게 타입이 지정되고 발견 가능한 기능 레지스트리 (capability registry)를 노출하는 역할만을 수행합니다.
- 전송 계층 (네트워크 프로토콜): 마이크로서비스가 HTTP/2 또는 gRPC를 통해 통신하는 것과 마찬가지로, MCP 통신은 로컬 프로세스를 위한 표준 입출력 (stdio) 스트림이나 원격 네트워크 서버를 위한 서버 전송 이벤트 (Server-Sent Events, SSE)와 같은 견고한 전송 메커니즘에 의존합니다.
AI 모델이 데이터가 필요하거나 작업을 실행해야 할 때, LLM 호스트는 커스텀 Python 또는 TypeScript 스크립트를 직접 실행하지 않습니다. 대신, 연결된 MCP 서버에 쿼리하여 사용 가능한 도구를 탐색하고, 해당 스키마 (schemas)를 검사한 뒤 실행을 위임합니다. MCP 서버는 요청을 처리하고, 로컬 시스템과 상호 작용하며, 표준화된 응답 페이로드 (response payload)를 반환합니다. 이러한 디커플링 (decoupling)은 보안 경계가 엄격하게 유지되도록 보장합니다. 즉, LLM은 사용자의 시스템에서 임의의 코드를 절대 실행하지 않으며, 단지 명시적이고 샌드박스화된 (sandboxed) MCP 도구가 사전 정의된 함수를 실행하도록 요청할 뿐입니다.
MCP의 구조: 전송 (Transports), JSON-RPC, 그리고 라이프사이클 관리 (Lifecycle Management)
핵심적으로 MCP는 복잡한 머신러닝 (Machine Learning) 프레임워크가 아닙니다. 그보다는 구조화된 입출력 스트림 (Input/Output Streams) 위에서 실행되는, 깔끔하고 엄격하게 설계된 **JSON-RPC 2.0 프로토콜 (Protocol)**입니다.
MCP 서버를 부팅할 때, 서버가 즉시 AI 토큰을 기다리며 대기하는 것은 아닙니다. 대신, 초기화 핸드셰이크 (Initialization Handshakes), 기능 협상 (Capability Negotiations), 그리고 지속적인 상태 동기화 (State Synchronization)에 의해 제어되는 정밀한 다단계 라이프사이클 (Lifecycle) 단계로 진입합니다.
1. 전송 계층 (The Transport Layer): Stdio 및 SSE
MCP 호스트 (Host)와 MCP 서버 (Server) 사이의 통신은 전송 메커니즘으로부터 분리되어야 합니다. TypeScript SDK는 추상화된 전송 인터페이스를 제공하며, 주로 다음 두 가지 모드를 지원합니다:
- Stdio 전송 (Stdio Transport): 로컬 개발을 위한 기본이자 가장 일반적인 모드입니다. MCP 호스트는
child_process.spawn을 사용하여 사용자의 TypeScript 서버를 자식 프로세스 (Child Process)로 생성합니다. 통신은 전적으로 표준 입력 (process.stdin) 및 표준 출력 (process.stdout)을 통해 이루어집니다. 이는 네트워크 포트를 열지 않기 때문에 매우 안전하며, 네트워크 기반의 취약점 클래스 전체를 제거합니다. 로그 및 디버깅 문구는 반드시 표준 에러 (process.stderr)로 엄격하게 라우팅되어야 합니다.stdout에 잘못 출력된console.log는 JSON-RPC 메시지 스트림을 손상시킬 수 있기 때문입니다. - SSE (Server-Sent Events) 전송: 원격 및 분산 아키텍처에 사용됩니다. 서버는 독립적인 HTTP 서비스로 실행되며, 클라이언트로 이벤트를 스트리밍하는 동시에 HTTP POST 요청을 통해 도구 실행 명령을 수락합니다.
2. 초기화 핸드셰이크 (The Initialization Handshake)
전송 채널이 설정되면, 어느 쪽도 상대방의 기능을 당연하게 가정하지 않습니다. 핸드셰이크는 다음과 같은 엄격한 JSON-RPC 메시지 시퀀스를 통해 진행됩니다:
initialize요청 (Request): 호스트(Host)는 자신의 프로토콜 버전과 클라이언트 기능(Capabilities)을 포함한initialize요청을 서버에 보냅니다.initialize응답 (Response): 서버는 자신의 프로토콜 버전, 서버 메타데이터(이름 및 버전), 그리고 지원하는 기능들의 선언 맵(Declarations map)을 응답합니다. 이때tools,resources, 또는prompts를 지원하는지 여부를 명시적으로 밝힙니다.notifications/initialized알림 (Notification): 호스트가 서버의 기능들을 수신하면, 초기화 단계가 완료되었으며 정상적인 운영 트래픽을 시작할 수 있음을 확인하는 최종 알림을 보냅니다.
3. 기능 분리: 도구 (Tools) vs. 리소스 (Resources)
MCP를 처음 접하는 개발자들이 흔히 혼동하는 지점은 **도구 (Tools)**와 **리소스 (Resources)**를 구분하는 것입니다. 두 기능 모두 LLM에 데이터를 노출하지만, 그 의미론적 계약 (Semantic contracts)은 근본적으로 다릅니다:
- 리소스 (Resources, 읽기 전용 데이터): 리소스는 LLM이 읽을 수 있는 정적 또는 동적 컨텍스트 데이터를 나타냅니다. 이를 URI(예:
postgres://users/schema또는file:///logs/error.log)로 식별되는 읽기 전용 파일, 데이터베이스 스키마, 또는 API 엔드포인트라고 생각하면 됩니다. 리소스는 수동적입니다. LLM은 답변을 구성하거나 어떤 도구를 호출할지 결정하기 전에 컨텍스트를 수집하기 위해 리소스를 읽습니다. - 도구 (Tools, 능동적 실행): 도구는 상태를 변경하거나 동작을 수행할 수 있는 기능을 나타냅니다. 데이터베이스에 기록하거나, 이메일을 보내거나, 셸 명령을 실행하거나, 실시간 가격 API를 쿼리하는 함수라고 생각하면 됩니다. 도구는 명시적인 입력값(인자, Arguments)을 필요로 하며 실행 결과를 반환합니다. 결정적으로, 도구는 능동적입니다. LLM은 사용자의 프롬프트를 기반으로 도구를 호출하기로 명시적으로 결정하며, 런타임에 검증된 구조화된 파라미터를 전달합니다.
엄격한 검증의 역할: 계약 집행자로서의 Zod
전통적인 웹 애플리케이션에서 입력 검증(Input Validation)은 보안과 데이터 무결성을 위해 중요합니다. AI 에이전트 아키텍처에서 엄격한 입력 검증은 절대적인 필수 사항입니다. 대규모 언어 모델(Large Language Models, LLM)은 확률적으로 텍스트를 생성하기 때문에, 데이터 타입, 필수 인자 누락, 또는 JSON 구조의 잘못된 형식 지정과 같은 미묘한 환각(Hallucination) 현상이 발생하기 쉽기 때문입니다.
TypeScript로 MCP 서버를 구축할 때, 들어오는 도구 인자(Tool Arguments)를 검증하기 위해 수동으로 if/else 체크를 작성하거나 임시 정규 표현식을 사용하지 않습니다. 대신, TypeScript 우선(TypeScript-first) 스키마 선언 및 검증 라이브러리인 Zod에 의존합니다.
MCP는 LLM의 도구 선언과 Zod를 통한 엄격한 런타임 스키마 강제(Runtime Schema Enforcement)를 결합함으로써, 확률적인 텍스트 생성과 결정론적인 백엔드 실행 사이의 간극을 메웁니다. TypeScript SDK를 사용하여 MCP 도구를 정의할 때, 도구의 메타데이터와 함께 Zod 스키마를 전달합니다:
// Zod가 런타임 안전성을 어떻게 강제하는지 보여주는 개념적 스키마 정의
const CreateUserSchema = z.object({
username: z.string().describe("사용자의 고유 핸들"),
...
내부적으로 MCP SDK는 도구 탐색(Tool Discovery) 단계에서 이러한 Zod 스키마를 표준 JSON Schema 명세로 직렬화(Serialize)합니다. LLM 호스트(Host)가 사용 가능한 도구 목록을 요청하면, 이 명시적인 JSON Schema들을 전달받게 됩니다. 그런 다음 호스트는 이 스키마들을 LLM의 시스템 프롬프트나 도구 호출 문법(OpenAI의 Function Calling 또는 Anthropic의 Tool Use 블록 등)에 주입하여, 모델의 출력 생성이 예상된 구조와 일치하도록 제한합니다.
LLM이 도구를 호출하기로 결정하면, 가공되지 않은 인자 페이로드(Raw Argument Payload)를 포함한 JSON-RPC 요청을 보냅니다. MCP 서버는 이 페이로드를 가로채어 Zod 스키마의 .parse() 또는 .safeParse() 메서드를 통해 직접 전달합니다:
- 타입 강제 변환 및 검증 (Type Coercion and Validation): Zod는 런타임(runtime)에 타입이 TypeScript 정의와 일치하는지 확인합니다. 만약 LLM이 정수
25대신 `
MCP 서버는 디바이스 드라이버 (Device Driver) (예: 그래픽 드라이버, 프린터 드라이버 또는 파일 시스템 드라이버)와 유사합니다. 운영 체제가 특수한 RAID 컨트롤러와 상호 작용하기 전에, 제조사는 운영 체제의 엄격한 커널 확장 인터페이스 (kernel extension interfaces)를 준수하는 드라이버를 작성해야 합니다. 이와 마찬가지로, AI 모델이 독점적인 SQL 데이터베이스나 로컬 Git 리포지토리 (Git repository)와 상호 작용하기 전에, 개발자는 Model Context Protocol 명세 (specification)를 준수하는 MCP 서버를 작성해야 합니다.
비유 2: GraphQL 리졸버 (Resolvers) vs. MCP 도구 핸들러 (Tool Handlers)
현대적인 웹 API를 구축한 경험이 있다면, MCP 서버의 아키텍처 설계가 매우 익숙하게 느껴질 것입니다. GraphQL 서버에서는 GraphQL SDL을 사용하여 스키마 (schema)를 정의하고, 타입 (types), 쿼리 (queries), 뮤테이션 (mutations)을 매핑합니다. 해당 스키마의 모든 필드에 대해, 데이터베이스, 마이크로서비스 (microservice) 또는 캐시 (cache)에서 필요한 데이터를 가져오는 **리졸버 함수 (resolver function)**를 작성합니다.
MCP 서버에서 도구 (tools)와 그 실행 핸들러 (execution handlers) 사이의 관계는 GraphQL 리졸버와 정확히 일치합니다:
- GraphQL 스키마 $\approx$ Zod 도구 정의 (Tool Definitions): 스키마는 어떤 데이터를 쿼리할 수 있는지, 어떤 뮤테이션을 실행할 수 있는지를 타입 시그니처 (type signatures) 및 필수 인자 (required arguments)와 함께 정의합니다.
- GraphQL 리졸버 $\approx$ MCP 도구 실행 콜백 (Tool Execution Callbacks): 리졸버는 ORM 연결, 외부 REST API 호출 또는 로컬 파일 시스템 조작과 같은 실제 명령형 TypeScript 로직 (imperative TypeScript logic)을 포함하며, 직렬화된 결과 (serialized result)를 반환합니다.
프로덕션 환경에 적합한 SaaS 지원 MCP 서버 구축하기
SaaS 환경을 위해 설계된 완전하고 독립적인 MCP 서버를 살펴보겠습니다. 이 예제는 시뮬레이션된 고객 지원 지표 및 사용자 조회 도구를 구현합니다. 이 서버는 공식 @modelcontextprotocol/sdk와 엄격한 런타임 (runtime) 입력 검증을 위한 zod를 사용하며, 표준 입출력 (stdio) 스트림을 통해 MCP 호스트 (host)와 통신합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기