본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 5장: TypeScript로 MCP 서버 구축하기
2026년 1월 29일·AI / ML·

5장: TypeScript로 MCP 서버 구축하기

TypeScript SDK를 사용하여 프로젝트 설정부터 도구, 리소스, 프롬프트 구현, 테스트까지 MCP 서버를 구축하는 전 과정을 다룹니다.

18분1,012자11개 섹션
mcptypescriptpython
공유
mcp-guide5 / 10
12345678910
이전4장: 서버 프리미티브 - 도구, 리소스, 프롬프트다음6장: Python FastMCP로 서버 구축하기

프로젝트 설정

TypeScript MCP 서버를 구축하기 위한 개발 환경을 준비합니다. 공식 SDK인 @modelcontextprotocol/sdk를 사용합니다.

프로젝트 초기화

프로젝트 생성
bash
mkdir weather-mcp-server && cd weather-mcp-server
npm init -y
의존성 설치
bash
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
  • @modelcontextprotocol/sdk: 공식 MCP TypeScript SDK
  • zod: 런타임 스키마 검증 라이브러리. SDK가 Zod를 사용하여 도구의 입력 스키마를 정의합니다.
  • tsx: TypeScript 파일을 직접 실행하기 위한 도구

TypeScript 설정

tsconfig.json
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}
package.json (핵심 부분)
json
{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx src/index.ts"
  }
}
Warning

package.json에 "type": "module"을 반드시 설정해야 합니다. MCP SDK는 ES 모듈 형식으로 배포되므로, CommonJS 환경에서는 import 에러가 발생합니다.

프로젝트 구조

weather-mcp-server/
  src/
    index.ts          # 서버 진입점
    tools/
      weather.ts      # 날씨 도구
      forecast.ts     # 예보 도구
    resources/
      locations.ts    # 지역 리소스
    prompts/
      weather-report.ts  # 날씨 리포트 프롬프트
  tsconfig.json
  package.json

McpServer 클래스 이해

MCP TypeScript SDK의 핵심은 McpServer 클래스입니다. 이 클래스는 도구, 리소스, 프롬프트를 등록하고, 전송 계층과 연결하는 고수준 API를 제공합니다.

src/index.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
 
const server = new McpServer({
  name: "weather-server",
  version: "1.0.0",
  description: "날씨 정보를 제공하는 MCP 서버입니다.",
});

McpServer는 내부적으로 Server 클래스(저수준 프로토콜 구현)를 래핑하고, 편리한 메서드(tool(), resource(), prompt())를 추가한 것입니다. 대부분의 경우 McpServer만으로 충분합니다.

도구 구현

기본 도구 등록

날씨 정보를 조회하는 도구를 구현합니다. server.tool() 메서드는 이름, 설명, 입력 스키마, 핸들러 함수를 받습니다.

src/tools/weather.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
 
export function registerWeatherTools(server: McpServer) {
  // 현재 날씨 조회
  server.tool(
    "get_current_weather",
    "지정된 도시의 현재 날씨 정보를 조회합니다. 온도, 습도, 날씨 상태 등을 반환합니다.",
    {
      city: z.string().describe("날씨를 조회할 도시 이름 (예: 서울, 부산)"),
      units: z
        .enum(["celsius", "fahrenheit"])
        .default("celsius")
        .describe("온도 단위"),
    },
    async (params) => {
      const weather = await fetchWeather(params.city, params.units);
 
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(weather, null, 2),
          },
        ],
      };
    }
  );
}
 
interface WeatherData {
  city: string;
  temperature: number;
  units: string;
  humidity: number;
  condition: string;
  wind_speed: number;
  observed_at: string;
}
 
async function fetchWeather(
  city: string,
  units: string
): Promise<WeatherData> {
  // 실제 구현에서는 외부 날씨 API를 호출합니다
  const mockData: Record<string, WeatherData> = {
    서울: {
      city: "서울",
      temperature: 18,
      units: units,
      humidity: 45,
      condition: "맑음",
      wind_speed: 3.2,
      observed_at: new Date().toISOString(),
    },
    부산: {
      city: "부산",
      temperature: 22,
      units: units,
      humidity: 60,
      condition: "흐림",
      wind_speed: 5.1,
      observed_at: new Date().toISOString(),
    },
  };
 
  const data = mockData[city];
  if (!data) {
    throw new Error(city + "의 날씨 정보를 찾을 수 없습니다.");
  }
 
  return data;
}

SDK는 Zod 스키마를 자동으로 JSON Schema로 변환하여 프로토콜에 노출합니다. 따라서 z.string().describe("설명")처럼 작성하면 JSON Schema의 description 필드에 자동 매핑됩니다.

에러 처리가 포함된 도구

src/tools/forecast.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
 
export function registerForecastTools(server: McpServer) {
  server.tool(
    "get_forecast",
    "지정된 도시의 향후 날씨 예보를 조회합니다. 최대 7일까지 예보를 제공합니다.",
    {
      city: z.string().describe("예보를 조회할 도시 이름"),
      days: z
        .number()
        .min(1)
        .max(7)
        .default(3)
        .describe("예보 일수 (1~7일)"),
    },
    async (params) => {
      try {
        const forecast = await fetchForecast(params.city, params.days);
 
        return {
          content: [
            {
              type: "text" as const,
              text: formatForecast(params.city, forecast),
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text" as const,
              text: "예보 조회에 실패했습니다: " + (error as Error).message,
            },
          ],
          isError: true,
        };
      }
    }
  );
}
 
interface ForecastDay {
  date: string;
  high: number;
  low: number;
  condition: string;
  precipitation: number;
}
 
async function fetchForecast(
  city: string,
  days: number
): Promise<ForecastDay[]> {
  // 실제 구현에서는 외부 API를 호출합니다
  const forecasts: ForecastDay[] = [];
  const conditions = ["맑음", "구름 조금", "흐림", "비", "눈"];
  const today = new Date();
 
  for (let i = 0; i < days; i++) {
    const date = new Date(today);
    date.setDate(date.getDate() + i + 1);
    forecasts.push({
      date: date.toISOString().split("T")[0],
      high: Math.round(15 + Math.random() * 15),
      low: Math.round(5 + Math.random() * 10),
      condition: conditions[Math.floor(Math.random() * conditions.length)],
      precipitation: Math.round(Math.random() * 80),
    });
  }
 
  return forecasts;
}
 
function formatForecast(city: string, forecast: ForecastDay[]): string {
  const lines = [city + " " + forecast.length + "일 예보:", ""];
  for (const day of forecast) {
    lines.push(
      day.date +
      ": " + day.condition +
      " (최고 " + day.high + "도 / 최저 " + day.low + "도" +
      ", 강수확률 " + day.precipitation + "%)"
    );
  }
  return lines.join("\n");
}
Tip

도구 핸들러에서 에러가 발생하면 두 가지 선택지가 있습니다. (1) 예외를 throw하면 프로토콜 수준의 에러 응답이 됩니다. (2) isError: true가 포함된 결과를 반환하면 LLM이 에러를 인식하고 사용자에게 적절히 안내합니다. 일반적으로 (2)가 더 나은 사용자 경험을 제공합니다. 사용자에게 안내 가능한 에러는 isError: true로, 복구 불가능한 시스템 에러는 예외로 처리하는 것을 권장합니다.

리소스 구현

정적 리소스

src/resources/locations.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 
export function registerResources(server: McpServer) {
  // 지원하는 도시 목록 리소스
  server.resource(
    "supported-cities",
    "weather://cities",
    { description: "날씨 정보를 제공하는 도시 목록", mimeType: "application/json" },
    async () => ({
      contents: [
        {
          uri: "weather://cities",
          text: JSON.stringify({
            cities: [
              { name: "서울", country: "KR", timezone: "Asia/Seoul" },
              { name: "부산", country: "KR", timezone: "Asia/Seoul" },
              { name: "제주", country: "KR", timezone: "Asia/Seoul" },
              { name: "인천", country: "KR", timezone: "Asia/Seoul" },
              { name: "대구", country: "KR", timezone: "Asia/Seoul" },
            ],
            total: 5,
            lastUpdated: new Date().toISOString(),
          }, null, 2),
          mimeType: "application/json",
        },
      ],
    })
  );
 
  // 서버 상태 리소스
  server.resource(
    "server-status",
    "weather://status",
    { description: "날씨 서버의 현재 상태 정보" },
    async () => ({
      contents: [
        {
          uri: "weather://status",
          text: JSON.stringify({
            status: "healthy",
            uptime: process.uptime(),
            version: "1.0.0",
            apiQuota: { used: 150, limit: 1000, resetAt: "2026-04-05T00:00:00Z" },
          }, null, 2),
        },
      ],
    })
  );
}

동적 리소스 (리소스 템플릿)

리소스 템플릿 등록
typescript
server.resourceTemplate(
  "city-detail",
  "weather://cities/{city}",
  { description: "특정 도시의 상세 정보", mimeType: "application/json" },
  async (uri, params) => {
    const cityName = params.city;
    const cityInfo = await getCityDetail(cityName);
 
    return {
      contents: [
        {
          uri: uri.toString(),
          text: JSON.stringify(cityInfo, null, 2),
          mimeType: "application/json",
        },
      ],
    };
  }
);

프롬프트 구현

src/prompts/weather-report.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
 
export function registerPrompts(server: McpServer) {
  server.prompt(
    "daily-weather-report",
    "특정 도시의 일일 날씨 리포트를 생성합니다",
    [
      { name: "city", description: "리포트 대상 도시", required: true },
      {
        name: "audience",
        description: "대상 독자 (general/farmer/traveler)",
        required: false,
      },
    ],
    async (params) => {
      const audienceGuide = {
        general: "일반 독자를 위한 간결한 날씨 요약을 제공하세요.",
        farmer: "농업 종사자를 위해 기온, 강수량, 습도 정보를 상세히 다루세요.",
        traveler: "여행자를 위해 야외 활동 적합성과 복장 추천을 포함하세요.",
      };
 
      const audience = params.audience || "general";
      const guide = audienceGuide[audience as keyof typeof audienceGuide]
        || audienceGuide.general;
 
      return {
        messages: [
          {
            role: "user" as const,
            content: {
              type: "text" as const,
              text: [
                params.city + "의 오늘 날씨 리포트를 작성해 주세요.",
                "",
                "작성 가이드:",
                "- " + guide,
                "- 날씨 데이터는 get_current_weather 도구를 사용하여 조회하세요.",
                "- 예보 정보는 get_forecast 도구를 사용하여 3일치를 조회하세요.",
                "- 리포트 형식: 제목, 현재 상황 요약, 향후 전망, 생활 조언",
              ].join("\n"),
            },
          },
        ],
      };
    }
  );
 
  server.prompt(
    "compare-cities",
    "여러 도시의 날씨를 비교합니다",
    [
      { name: "cities", description: "비교할 도시 목록 (쉼표 구분)", required: true },
    ],
    async (params) => ({
      messages: [
        {
          role: "user" as const,
          content: {
            type: "text" as const,
            text: [
              "다음 도시들의 날씨를 비교 분석해 주세요: " + params.cities,
              "",
              "각 도시의 현재 날씨를 get_current_weather 도구로 조회한 후,",
              "기온, 습도, 날씨 상태를 비교하는 표를 작성하고,",
              "여행이나 야외 활동에 가장 적합한 도시를 추천해 주세요.",
            ].join("\n"),
          },
        },
      ],
    })
  );
}

서버 조립과 실행

모든 프리미티브를 하나의 서버로 조립합니다.

src/index.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerWeatherTools } from "./tools/weather.js";
import { registerForecastTools } from "./tools/forecast.js";
import { registerResources } from "./resources/locations.js";
import { registerPrompts } from "./prompts/weather-report.js";
 
const server = new McpServer({
  name: "weather-server",
  version: "1.0.0",
  description: "날씨 정보를 제공하는 MCP 서버입니다.",
});
 
// 프리미티브 등록
registerWeatherTools(server);
registerForecastTools(server);
registerResources(server);
registerPrompts(server);
 
// 전송 계층 연결 및 서버 시작
const transport = new StdioServerTransport();
await server.connect(transport);
 
console.error("Weather MCP Server가 실행되었습니다.");
Info

console.error()를 사용하여 로그를 출력하는 것에 주의하세요. stdio 전송에서 console.log()(stdout)는 프로토콜 메시지 채널이므로, 로그를 stdout에 출력하면 프로토콜 메시지와 혼합되어 파싱 에러가 발생합니다. 로그는 반드시 stderr에 출력해야 합니다.

Claude Desktop에 연결하기

구축한 서버를 Claude Desktop에 연결합니다. Claude Desktop의 설정 파일에 MCP 서버 정보를 추가합니다.

~/Library/Application Support/Claude/claude_desktop_config.json
json
{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/absolute/path/to/weather-mcp-server/dist/index.js"]
    }
  }
}

개발 중에는 tsx를 사용하여 빌드 없이 직접 실행할 수도 있습니다.

개발용 설정
json
{
  "mcpServers": {
    "weather": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/weather-mcp-server/src/index.ts"]
    }
  }
}

설정 파일을 수정한 후 Claude Desktop을 재시작하면, 날씨 서버의 도구들이 사용 가능해집니다.

MCP Inspector로 테스트하기

MCP Inspector는 MCP 서버를 독립적으로 테스트할 수 있는 개발 도구입니다. 서버의 도구, 리소스, 프롬프트를 직접 호출하고 결과를 확인할 수 있습니다.

MCP Inspector 실행
bash
npx @modelcontextprotocol/inspector node dist/index.js

Inspector는 웹 브라우저에서 실행되며, 다음 작업을 수행할 수 있습니다.

  • 도구 목록 조회: 등록된 모든 도구와 스키마를 확인합니다.
  • 도구 호출: 매개변수를 입력하고 도구를 실행하여 결과를 확인합니다.
  • 리소스 읽기: 등록된 리소스를 읽어 내용을 확인합니다.
  • 프롬프트 조회: 프롬프트를 매개변수와 함께 조회하여 생성되는 메시지를 확인합니다.
  • 프로토콜 메시지 감시: 클라이언트와 서버 간에 교환되는 모든 JSON-RPC 메시지를 실시간으로 확인합니다.
Tip

MCP Inspector는 서버 개발 과정에서 매우 유용한 도구입니다. LLM을 거치지 않고 도구를 직접 호출할 수 있으므로, 서버 로직의 정확성을 빠르게 검증할 수 있습니다. 개발 초기에 Inspector로 충분히 테스트한 후 LLM 연동 테스트로 넘어가는 것을 권장합니다.

단위 테스트 작성

MCP 서버의 도구 로직을 단위 테스트로 검증할 수 있습니다. SDK의 Client를 사용하여 프로그래밍 방식으로 서버에 연결합니다.

src/__tests__/weather.test.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { registerWeatherTools } from "../tools/weather.js";
 
async function createTestClient(): Promise<Client> {
  const server = new McpServer({ name: "test-server", version: "0.0.1" });
  registerWeatherTools(server);
 
  const client = new Client({ name: "test-client", version: "0.0.1" });
 
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
  await Promise.all([
    client.connect(clientTransport),
    server.connect(serverTransport),
  ]);
 
  return client;
}
 
// 테스트 실행
async function runTests() {
  const client = await createTestClient();
 
  // 도구 목록 확인
  const tools = await client.listTools();
  console.assert(
    tools.tools.some((t) => t.name === "get_current_weather"),
    "get_current_weather 도구가 존재해야 합니다"
  );
 
  // 도구 호출
  const result = await client.callTool({
    name: "get_current_weather",
    arguments: { city: "서울" },
  });
 
  const text = (result.content as Array<{ type: string; text: string }>)[0].text;
  const data = JSON.parse(text);
  console.assert(data.city === "서울", "도시 이름이 일치해야 합니다");
  console.assert(typeof data.temperature === "number", "온도가 숫자여야 합니다");
 
  console.error("모든 테스트를 통과했습니다.");
}
 
runTests().catch(console.error);

InMemoryTransport는 네트워크나 프로세스 없이 클라이언트와 서버를 메모리 내에서 연결합니다. 단위 테스트에 이상적인 전송 방식입니다.

정리

이 장에서는 TypeScript SDK를 사용하여 완전한 MCP 서버를 구축했습니다.

  • 프로젝트 설정: @modelcontextprotocol/sdk와 Zod를 사용한 TypeScript 프로젝트 구성
  • 도구 구현: Zod 스키마 기반의 도구 정의와 에러 처리
  • 리소스 구현: 정적 리소스와 리소스 템플릿
  • 프롬프트 구현: 동적 매개변수를 받는 워크플로우 템플릿
  • 테스트: MCP Inspector와 InMemoryTransport를 활용한 테스트
  • 연동: Claude Desktop 설정 파일을 통한 서버 연결

다음 장 미리보기

6장에서는 Python FastMCP 프레임워크를 사용하여 동일한 기능의 서버를 구축합니다. 데코레이터 기반의 직관적인 API와 비동기 지원을 활용하여 더 간결한 코드로 서버를 구현하는 방법을 다루겠습니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#mcp#typescript#python

관련 글

AI / ML

6장: Python FastMCP로 서버 구축하기

Python의 FastMCP 프레임워크를 사용하여 데코레이터 기반의 간결하고 직관적인 MCP 서버를 구축하는 방법을 다룹니다.

2026년 1월 31일·17분
AI / ML

4장: 서버 프리미티브 - 도구, 리소스, 프롬프트

MCP 서버가 제공하는 세 가지 핵심 프리미티브의 스키마 정의, 구현 패턴, 실전 활용 사례를 상세히 다룹니다.

2026년 1월 27일·24분
AI / ML

7장: MCP 클라이언트 구현하기

MCP 클라이언트를 직접 구현하여 서버에 연결하고, LLM과 통합하여 도구 호출 파이프라인을 완성하는 방법을 다룹니다.

2026년 2월 2일·15분
이전 글4장: 서버 프리미티브 - 도구, 리소스, 프롬프트
다음 글6장: Python FastMCP로 서버 구축하기

댓글

목차

약 18분 남음
  • 프로젝트 설정
    • 프로젝트 초기화
    • TypeScript 설정
    • 프로젝트 구조
  • McpServer 클래스 이해
  • 도구 구현
    • 기본 도구 등록
    • 에러 처리가 포함된 도구
  • 리소스 구현
    • 정적 리소스
    • 동적 리소스 (리소스 템플릿)
  • 프롬프트 구현
  • 서버 조립과 실행
  • Claude Desktop에 연결하기
  • MCP Inspector로 테스트하기
  • 단위 테스트 작성
  • 정리
  • 다음 장 미리보기