TypeScript로 처음부터 MCP 서버 구축하기
요약
TypeScript를 사용하여 프로덕션 수준의 Model Context Protocol(MCP) 서버를 구축하는 방법을 다룹니다. Tools, Resources, Prompts의 세 가지 핵심 요소를 구현하며, stdio 및 HTTP 전송 방식을 모두 지원합니다.
핵심 포인트
- stdio 전송 시 로깅은 반드시 stderr를 사용해야 함
- MCP 서버는 독립적인 프로세스로 실행되어야 함
- SDK를 통해 JSON-RPC 및 기능 협상을 자동으로 처리 가능
- Tools, Resources, Prompts를 통한 핵심 기능 구현 방법 제시
Model Context Protocol (MCP)는 문서화 측면에서 파편화 문제를 겪고 있습니다. 대부분의 예제는 유용하기에는 너무 최소적이거나, 프레임워크의 보일러플레이트 (Boilerplate) 아래에 핵심 개념을 묻어버립니다. 이 글에서는 TypeScript를 사용하여 실제 MCP 서버를 구축하는 과정을 살펴봅니다. 이 서버는 세 가지 기본 요소(Tools, Resources, Prompts)를 모두 구현하며, stdio 및 HTTP 전송 (Transport) 방식 모두에서 실행됩니다.
이 코드는 장난감 수준이 아닌, 프로덕션 (Production) 수준에 맞춰 작성되었습니다. 글을 마칠 때쯤이면 무엇을 작성해야 하는지뿐만 아니라, 왜 각 구성 요소가 그런 방식으로 작성되어야 하는지도 이해하게 될 것입니다.
stdio에 대해 반드시 알아야 할 한 가지
코드를 작성하기 전에: 만약 stdio 전송 방식을 사용한다면, 모든 로깅은 반드시 stderr로 보내야 합니다. console.log가 아니라 console.error를 사용해야 합니다.
stdio 전송 방식은 stdout을 JSON-RPC 프레임의 프로토콜 채널로 사용합니다. 단 한 번의 console.log만으로도 프레임이 손상되어 호스트의 파싱 (Parsing) 실패를 유발합니다. SDK는 이에 대해 경고를 주지 않습니다. 호스트는 그냥 조용히 작동을 멈출 뿐입니다.
// ✅ 올바른 방법
console.error('Server started');
...
이 부분은 거의 모든 사람이 처음 접할 때 실수하는 지점입니다. 이제 여러분은 알고 있습니다.
프로젝트 설정
MCP 서버는 독립적인 프로세스로 실행됩니다. 즉, 기존 웹 서버와 프로세스를 공유하지 않습니다. 전용 패키지를 생성하세요:
{
"name": "@ts-ai/mcp-server",
"version": "1.0.0",
...
npm에 게시할 계획이라면 bin 필드가 중요합니다. 사용자가 서버를 전역으로 설치하지 않고도 npx ts-ai-mcp로 서버를 실행할 수 있기 때문입니다.
최소 기능 서버
SDK가 프로토콜의 세부 사항(JSON-RPC 직렬화 (Serialization), 핸드셰이크 (Handshake), 기능 협상 (Capability negotiation), 메시지 라우팅 (Message routing))을 처리합니다. 여러분은 핸들러를 등록하고 데이터를 반환하기만 하면 됩니다.
// src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
...
server.connect()는 논블로킹 (Non-blocking) 방식입니다. 이는 stdin/stdout을 제어권을 가져온 뒤 즉시 반환됩니다. 프로세스는 stdin이 열려 있는 동안 Node.js의 이벤트 루프 (Event loop)를 통해 계속 살아있습니다. setInterval이나 다른 keep-alive 트릭을 사용할 필요가 없습니다.
또한 SDK는 어떤 등록 메서드(server.tool(), server.resource(), server.prompt())를 호출하느냐에 따라 기능(capabilities)을 자동으로 추론합니다. 수동으로 기능을 선언할 필요가 없습니다.
도구 (Tools): 핵심 프리미티브 (Core Primitive)
도구는 LLM이 행동을 취하기 위해 호출할 수 있는 요소입니다. SDK는 도구 선언과 실행을 단일 server.tool() 호출로 통합합니다.
// src/handlers/tools.ts
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { McpError } from '@modelcontextprotocol/sdk/types.js';
...
에러 핸들링(error handling) 패턴은 의도된 것입니다. 도구가 실패했을 때, McpError를 던지는(throw) 것보다 메시지와 함께 isError: true를 반환하는 것이 거의 항상 더 낫습니다. LLM은 에러를 읽고 다음에 무엇을 할지 결정할 수 있습니다. 프로토콜 에러를 던지는 것은 단순히 대화를 끊어버릴 뿐입니다.
리소스 (Resources): 컨텍스트 주입 (Context Injection)
리소스는 호스트가 LLM의 컨텍스트에 콘텐츠를 주입할 수 있게 합니다. 두 가지 패턴이 있습니다: 고정된 리소스를 위한 정적 URI (static URIs), 동적인 리소스를 위한 URI 템플릿 (URI templates)입니다.
// src/handlers/resources.ts
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
...
URI 스킴(knowledge-base://)은 사용자가 정의할 수 있습니다. 의미론적 접두사(semantic prefix)를 사용하면 동일한 호스트에서 실행되는 다른 MCP 서버의 리소스와 충돌하는 것을 방지할 수 있습니다.
프롬프트 (Prompts): 재사용 가능한 지침 템플릿 (Reusable Instruction Templates)
프롬프트는 매번 새로 작성해야 할 다단계 지침들을 캡슐화합니다. 시스템 프롬프트(system prompt)와의 핵심적인 차이점은, 매개변수(parameters)를 수용하고 LLM이 수행할 전체 messages 배열을 반환한다는 점입니다.
// src/handlers/prompts.ts
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
...
messages 배열에는 도구 호출(tool invocation) 지침이 포함될 수 있습니다. LLM은 이를 자동으로 따르며, 매번 사용자의 수동적인 안내가 필요하지 않습니다.
한 가지 주의할 점은 프롬프트(prompt)가 트리거되는 방식이 호스트(host)마다 다르다는 것입니다. Claude Desktop은 MCP 프롬프트를 위한 슬래시 명령(slash commands)을 노출하지 않지만, Claude Code는 지원합니다. 대상 호스트의 문서를 확인하세요.
연결하기 (Wiring It Together)
// src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
...
호스트 없이 디버깅하기
서버를 테스트하기 위해 Claude Desktop이 반드시 필요한 것은 아닙니다. 개발에는 MCP Inspector가 더 적합합니다:
npx @modelcontextprotocol/inspector node dist/index.js
Inspector는 서버를 자식 프로세스(child process)로 실행하고 브라우저 UI를 엽니다. 여기에서 도구(tool)를 호출하고, 리소스(resource)를 읽고, 프롬프트 템플릿(prompt template)을 대화형으로 가져올 수 있으며, 가공되지 않은 JSON-RPC 프레임(frames)도 확인할 수 있습니다. 호스트 설정이 필요하지 않습니다.
Claude Desktop에서 검증할 준비가 되면 ~/Library/Application Support/Claude/claude_desktop_config.json에 다음을 추가하세요:
{
"mcpServers": {
"ts-ai": {
...
tsx를 사용하면 개발 중에 빌드 단계(build step)가 필요하지 않습니다. 변경 사항을 적용한 후에는 Claude Desktop을 재시작하세요.
원격 액세스를 위한 HTTP 트랜스포트 (HTTP Transport)
stdio는 로컬 전용입니다. 원격 또는 다중 사용자 시나리오의 경우 StreamableHTTPServerTransport를 사용하세요. 결정적인 차이점은 MCP가 상태 유지(stateful) 방식이기 때문에 sessionId → transport 매핑을 직접 유지해야 한다는 것입니다. initialize 이후의 모든 요청은 동일한 트랜스포트 인스턴스로 라우팅되어야 합니다.
// src/server-http.ts
import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
...
/sse 및 /messages 엔드포인트를 사용하는 예시를 본다면, 그것은 이전 SDK 패턴입니다. StreamableHTTPServerTransport는 이 두 가지를 하나의 POST /mcp 엔드포인트로 통합합니다.
여전히 놀라운 세 가지 사실
server.connect()는 논블로킹(non-blocking) 방식입니다. 프로세스는 해당 라인에서 차단되지 않습니다. 메시지 처리는 SDK의 내부 이벤트 루프(event loop)를 통해 비동기적으로 실행됩니다.
기능 선언(Capability declaration)은 자동으로 이루어집니다. SDK는 사용자가 호출하는 메서드를 통해 기능을 추론합니다. 대부분의 경우 capabilities: { tools: {}, resources: {} }와 같은 수동 설정이 필요하지 않습니다.
모든 호스트가 모든 MCP 기능을 지원하는 것은 아닙니다. 리소스 구독(Resource subscriptions)과 프롬프트 슬래시 명령어(Prompt slash commands)는 Claude Code에서는 작동하지만, Claude Desktop에서는 작동하지 않습니다. 특정 기능을 사용하여 개발하기 전에 대상 호스트를 확인하세요.
이 기사는 Leanpub 또는 Amazon에 게시된 AI Engineering with TypeScript — A Comprehensive Guide to Building AI Agents의 제19장을 바탕으로 작성되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기