MCP 서버 만들기

원문: https://modelcontextprotocol.io/docs/develop/build-server

이 튜토리얼에서는 간단한 MCP 날씨 서버를 만들고, Claude Desktop에 연결합니다.

만들게 될 것

두 가지 도구 get_alertsget_forecast를 제공하는 서버를 만들고, MCP 호스트(여기서는 Claude Desktop)에 연결합니다.

날씨 서버 도구 결과 예시
서버는 어떤 클라이언트와도 연결할 수 있습니다. 여기서는 간단히 Claude Desktop을 사용했지만, 클라이언트 만들기 가이드와 다른 클라이언트 목록도 참고하세요.

핵심 MCP 개념

MCP 서버는 다음 세 가지 기능을 제공할 수 있습니다.

  1. 리소스(Resources): 파일과 유사한 데이터(예: API 응답, 파일 내용)
  2. 도구(Tools): LLM이 호출할 수 있는 함수(사용자 승인 포함)
  3. 프롬프트(Prompts): 작업을 돕는 미리 작성된 템플릿

이 튜토리얼은 주로 도구에 집중합니다.

Python으로 시작하기

이 가이드는 Python에 익숙하고, Claude 같은 LLM을 사용해본 경험을 가정합니다.

MCP 서버 로깅 주의사항

STDIO 기반 서버에서는 stdout에 로그를 쓰면 JSON‑RPC 메시지가 깨집니다. 따라서 print()는 기본적으로 stdout을 사용하므로 피해야 하며, 필요하면 file=sys.stderr로 기록해야 합니다. HTTP 기반 서버는 stdout을 사용해도 괜찮습니다.

베스트 프랙티스

간단 예시

import sys
import logging

# ❌ Bad (STDIO)
print("Processing request")

# ✅ Good (STDIO)
print("Processing request", file=sys.stderr)

# ✅ Good (STDIO)
logging.info("Processing request")

시스템 요구사항

환경 설정

먼저 uv를 설치하고 프로젝트를 초기화합니다.

curl -LsSf https://astral.sh/uv/install.sh | sh
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

설치 후에는 터미널을 재시작해 uv가 인식되도록 합니다.

# Create a new directory for our project
uv init weather
cd weather

# Create virtual environment and activate it
uv venv
source .venv/bin/activate

# Install dependencies
uv add "mcp[cli]" httpx

# Create our server file
touch weather.py
# Create a new directory for our project
uv init weather
cd weather

# Create virtual environment and activate it
uv venv
.venv\Scripts\activate

# Install dependencies
uv add mcp[cli] httpx

# Create our server file
new-item weather.py

서버 구현

패키지 임포트 및 인스턴스 설정

weather.py 상단에 다음을 추가합니다.

from typing import Any

import httpx
from mcp.server.fastmcp import FastMCP

# Initialize FastMCP server
mcp = FastMCP("weather")

# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"

FastMCP는 타입 힌트와 docstring을 사용해 도구 정의를 자동 생성합니다.

헬퍼 함수

National Weather Service API 호출과 포맷을 위한 헬퍼 함수를 추가합니다.

async def make_nws_request(url: str) -> dict[str, Any] | None:
    """Make a request to the NWS API with proper error handling."""
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            return None


def format_alert(feature: dict) -> str:
    """Format an alert feature into a readable string."""
    props = feature["properties"]
    return f"""
Event: {props.get("event", "Unknown")}
Area: {props.get("areaDesc", "Unknown")}
Severity: {props.get("severity", "Unknown")}
Description: {props.get("description", "No description available")}
Instructions: {props.get("instruction", "No specific instructions provided")}
"""

도구 실행 구현

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g. CA, NY)
    """
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"
    data = await make_nws_request(url)

    if not data or "features" not in data:
        return "Unable to fetch alerts or no alerts found."

    if not data["features"]:
        return "No active alerts for this state."

    alerts = [format_alert(feature) for feature in data["features"]]
    return "\n---\n".join(alerts)


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a location.

    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location
    """
    # First get the forecast grid endpoint
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return "Unable to fetch forecast data for this location."

    # Get the forecast URL from the points response
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "Unable to fetch detailed forecast."

    # Format the periods into a readable forecast
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    for period in periods[:5]:  # Only show next 5 periods
        forecast = f"""
{period["name"]}:
Temperature: {period["temperature"]}°{period["temperatureUnit"]}
Wind: {period["windSpeed"]} {period["windDirection"]}
Forecast: {period["detailedForecast"]}
"""
        forecasts.append(forecast)

    return "\n---\n".join(forecasts)

메인 실행부

if __name__ == "__main__":
    mcp.run()

서버 실행

uv run weather.py

테스트 (Inspector 사용)

MCP Inspector로 서버를 테스트할 수 있습니다.

npx @modelcontextprotocol/inspector uv run weather.py

STDIO 전송 사용

STDIO 전송 예시

Claude Desktop에 연결하기

Claude Desktop 설정에서 Developer 탭 → Edit Config로 이동합니다.

Developer 설정 화면

설정 파일에 다음을 추가합니다.

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": ["run", "weather.py"]
    }
  }
}

재시작 후 입력창의 서버 표시에서 도구를 확인할 수 있습니다.

도구 목록

도구 테스트

예시: 캘리포니아의 경보와 샌프란시스코 예보 확인

"Get weather alerts for CA" and "Get forecast for San Francisco"
도구 실행 결과 예시

다음 단계