콘텐츠로 이동

서버 만들기 (Build a Server)

이 튜토리얼에서는 간단한 MCP 날씨 서버를 만들고, 호스트인 Claude for Desktop에 연결해 볼 거예요.

무엇을 만들까요

get_alertsget_forecast 두 도구를 노출하는 서버를 만들고, 이 서버를 MCP 호스트(여기서는 Claude for Desktop)에 연결할 거예요.

서버는 어떤 클라이언트와도 연결할 수 있어요. 여기서는 편의상 Claude for Desktop을 골랐을 뿐이고, 직접 클라이언트를 만드는 방법은 클라이언트 만들기 가이드에서 다룹니다.

핵심 MCP 개념

MCP 서버는 크게 세 가지 유형의 기능을 제공할 수 있어요.

  1. Resources: 클라이언트가 읽을 수 있는 파일 같은 데이터(API 응답이나 파일 내용처럼)
  2. Tools: LLM이 (사용자 승인을 받아) 호출할 수 있는 함수
  3. Prompts: 사용자가 특정 작업을 수행하도록 돕는 미리 작성된 템플릿

이 튜토리얼은 주로 도구(Tools)에 집중해요.

Python, TypeScript, Java, Kotlin, C#, Ruby, Rust, Go 언어별 탭이 준비되어 있어요.

자, 날씨 서버 만들기를 시작해 볼게요. 여기서 우리가 만들 완성 코드를 확인할 수 있어요.

사전 지식

이 퀵스타트는 다음에 익숙하다고 가정해요.

  • Python
  • Claude 같은 LLM

Python

사전 지식

이 퀵스타트는 다음에 익숙하다고 가정해요.

  • Python
  • Claude 같은 LLM

MCP 서버에서의 로깅

MCP 서버를 구현할 때는 로깅을 어떻게 처리할지 조심해야 해요.

  • STDIO 기반 서버: 절대 stdout에 쓰면 안 됩니다. stdout에 쓰면 JSON-RPC 메시지가 깨지고 서버가 망가져요. print() 함수는 기본적으로 stdout에 쓰므로, STDIO 서버에서는 아예 사용하지 않는 게 좋아요.
  • HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.

모범 사례

  • 표준 라이브러리 logging 모듈을 사용하세요. 이 모듈은 stderr에 씁니다.
  • 모듈마다 logging.getLogger(__name__)으로 로거를 하나 만들고 도구에서 호출하세요.

빠른 예시

import logging

logger = logging.getLogger(__name__)

# ❌ 나쁜 예 (STDIO)
print("Processing request")

# ✅ 좋은 예 (STDIO)
logger.info("Processing request")  # writes to stderr

시스템 요구 사항

  • Python 3.10 이상이 설치되어 있어야 해요.
  • Python MCP SDK 2.0.0 이상을 사용해야 합니다.

환경 설정

먼저 uv를 설치하고 Python 프로젝트와 환경을 준비할게요.

macOS/Linux

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows

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

설치 후에는 터미널을 다시 시작해야 uv 명령이 인식됩니다. 이제 프로젝트를 만들고 설정해 볼게요.

macOS/Linux

# 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]"

# Create our server file
touch weather.py

Windows

# 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]

# Create our server file
new-item weather.py

이제 본격적으로 서버를 만들어 볼게요.

서버 만들기

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

weather.py 맨 위에 다음을 추가해 주세요.

from typing import Any

import httpx2
from mcp.server import MCPServer

# Initialize MCPServer
mcp = MCPServer("weather")

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

httpx2는 SDK 자체가 의존하는 HTTP 클라이언트라서, mcp만 설치해도 이미 함께 딸려 옵니다. MCPServer 클래스는 Python 타입 힌트와 docstring을 활용해 도구 정의를 자동으로 생성해요. 그래서 MCP 도구를 쉽게 만들고 유지할 수 있어요.

헬퍼 함수

다음으로 국립기상청(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 httpx2.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(transport="stdio")

서버가 완성됐어요! uv run weather.py로 MCP 서버를 시작하면, MCP 호스트가 보내는 메시지를 기다리기 시작해요. 이제 기존 MCP 호스트인 Claude for Desktop에서 서버를 테스트해 볼게요.

Claude for Desktop으로 서버 테스트하기

먼저 Claude for Desktop이 설치되어 있어야 해요. 여기서 최신 버전을 설치할 수 있고, 이미 설치되어 있다면 최신 버전으로 업데이트했는지 확인해 주세요.

사용할 MCP 서버마다 Claude for Desktop을 설정해야 해요. Claude for Desktop 앱 설정 파일인 ~/Library/Application Support/Claude/claude_desktop_config.json을 텍스트 편집기로 열면 됩니다. 파일이 없으면 직접 만들어 주세요.

VS Code가 설치되어 있다면 이렇게 열 수 있어요.

Linux

code ~/.config/Claude/claude_desktop_config.json

macOS

code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows

code $env:AppData\Claude\claude_desktop_config.json

그다음 mcpServers 키에 서버를 추가하면 돼요. MCP UI 요소는 서버가 최소 하나 이상 제대로 설정되어 있을 때만 Claude for Desktop에 나타나요. 이번에는 날씨 서버 하나를 이렇게 추가해 볼게요.

macOS/Linux

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
        "run",
        "weather.py"
      ]
    }
  }
}

Windows

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
        "run",
        "weather.py"
      ]
    }
  }
}

command 필드에 uv 실행 파일의 전체 경로를 넣어야 할 수도 있어요. macOS/Linux에서는 which uv, Windows에서는 where uv를 실행하면 경로를 알 수 있습니다.

서버에는 절대 경로를 전달해야 해요. macOS/Linux에서는 pwd, Windows 명령 프롬프트에서는 cd로 확인할 수 있어요. Windows에서는 JSON 경로에 이중 백슬래시(\\)나 슬래시(/)를 사용해야 합니다.

이 설정이 Claude for Desktop에 알려주는 내용은 두 가지예요.

  1. "weather"라는 MCP 서버가 있다.
  2. uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather run weather.py로 실행한다.

파일을 저장하고 Claude for Desktop을 다시 시작해 주세요.

TypeScript

이 튜토리얼의 완성 코드는 여기에서 볼 수 있어요. 날씨 서버 만들기를 시작해 볼게요.

사전 지식

이 퀵스타트는 다음에 익숙하다고 가정해요.

  • TypeScript
  • Claude 같은 LLM

MCP 서버에서의 로깅

MCP 서버를 구현할 때는 로깅을 어떻게 처리할지 조심해야 해요.

  • STDIO 기반 서버: console.log()를 사용하면 안 됩니다. 기본적으로 표준 출력(stdout)에 쓰거든요. stdout에 쓰면 JSON-RPC 메시지가 깨지고 서버가 망가져요.
  • HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.

모범 사례

  • stderr에 쓰는 console.error()를 사용하거나, stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.

빠른 예시

// ❌ 나쁜 예 (STDIO)
console.log("Server started");

// ✅ 좋은 예 (STDIO)
console.error("Server started"); // stderr is safe

시스템 요구 사항

TypeScript의 경우 최신 버전의 Node가 설치되어 있어야 해요.

환경 설정

먼저 Node.js와 npm을 아직 설치하지 않았다면 설치해 주세요. nodejs.org에서 다운로드할 수 있습니다. Node.js 설치를 확인해 볼게요.

node --version
npm --version

이 튜토리얼에서는 Node.js 버전 20 이상이 필요해요. 이제 프로젝트를 만들고 설정해 볼게요.

macOS/Linux

# Create a new directory for our project
mkdir weather
cd weather

# Initialize a new npm project
npm init -y

# Install dependencies
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript

# Create our files
mkdir src
touch src/index.ts

Windows

# Create a new directory for our project
md weather
cd weather

# Initialize a new npm project
npm init -y

# Install dependencies
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript

# Create our files
md src
new-item src\index.ts

package.jsontype: "module"과 빌드 스크립트를 추가해 주세요.

package.json

{
  "type": "module",
  "bin": {
    "weather": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js"
  },
  "files": ["build"]
}

프로젝트 루트에 tsconfig.json을 만들어 주세요.

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "types": ["node"],
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

이제 본격적으로 서버를 만들어 볼게요.

서버 만들기

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

src/index.ts 맨 위에 다음을 추가해 주세요.

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";

const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";

// Create server instance
const server = new McpServer({
  name: "weather",
  version: "1.0.0",
});

헬퍼 함수

다음으로 국립기상청 API에서 데이터를 조회하고 포맷하는 헬퍼 함수를 추가할게요.

// Helper function for making NWS API requests
async function makeNWSRequest<T>(url: string): Promise<T | null> {
  const headers = {
    "User-Agent": USER_AGENT,
    Accept: "application/geo+json",
  };

  try {
    const response = await fetch(url, { headers });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return (await response.json()) as T;
  } catch (error) {
    console.error("Error making NWS request:", error);
    return null;
  }
}

interface AlertFeature {
  properties: {
    event?: string;
    areaDesc?: string;
    severity?: string;
    status?: string;
    headline?: string;
  };
}

// Format alert data
function formatAlert(feature: AlertFeature): string {
  const props = feature.properties;
  return [
    `Event: ${props.event || "Unknown"}`,
    `Area: ${props.areaDesc || "Unknown"}`,
    `Severity: ${props.severity || "Unknown"}`,
    `Status: ${props.status || "Unknown"}`,
    `Headline: ${props.headline || "No headline"}`,
    "---",
  ].join("\n");
}

interface ForecastPeriod {
  name?: string;
  temperature?: number;
  temperatureUnit?: string;
  windSpeed?: string;
  windDirection?: string;
  shortForecast?: string;
}

interface AlertsResponse {
  features: AlertFeature[];
}

interface PointsResponse {
  properties: {
    forecast?: string;
  };
}

interface ForecastResponse {
  properties: {
    periods: ForecastPeriod[];
  };
}

도구 실행 구현

도구 실행 핸들러는 각 도구의 실제 로직을 실행하는 역할을 해요. 추가해 볼게요.

// Register weather tools

server.registerTool(
  "get_alerts",
  {
    description: "Get weather alerts for a state",
    inputSchema: z.object({
      state: z
        .string()
        .length(2)
        .describe("Two-letter state code (e.g. CA, NY)"),
    }),
  },
  async ({ state }) => {
    const stateCode = state.toUpperCase();
    const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
    const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);

    if (!alertsData) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to retrieve alerts data",
          },
        ],
      };
    }

    const features = alertsData.features || [];
    if (!features.length) {
      return {
        content: [
          {
            type: "text",
            text: `No active alerts for ${stateCode}`,
          },
        ],
      };
    }

    const formattedAlerts = features.map(formatAlert);
    const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;

    return {
      content: [
        {
          type: "text",
          text: alertsText,
        },
      ],
    };
  },
);

server.registerTool(
  "get_forecast",
  {
    description: "Get weather forecast for a location",
    inputSchema: z.object({
      latitude: z
        .number()
        .min(-90)
        .max(90)
        .describe("Latitude of the location"),
      longitude: z
        .number()
        .min(-180)
        .max(180)
        .describe("Longitude of the location"),
    }),
  },
  async ({ latitude, longitude }) => {
    // Get grid point data
    const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
    const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);

    if (!pointsData) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
          },
        ],
      };
    }

    const forecastUrl = pointsData.properties?.forecast;
    if (!forecastUrl) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to get forecast URL from grid point data",
          },
        ],
      };
    }

    // Get forecast data
    const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
    if (!forecastData) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to retrieve forecast data",
          },
        ],
      };
    }

    const periods = forecastData.properties?.periods || [];
    if (periods.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: "No forecast periods available",
          },
        ],
      };
    }

    // Format forecast periods
    const formattedForecast = periods.map((period: ForecastPeriod) =>
      [
        `${period.name || "Unknown"}:`,
        `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
        `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
        `${period.shortForecast || "No forecast available"}`,
        "---",
      ].join("\n"),
    );

    const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;

    return {
      content: [
        {
          type: "text",
          text: forecastText,
        },
      ],
    };
  },
);

서버 실행

마지막으로 서버를 실행하는 main 함수를 구현합니다.

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Weather MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

서버를 빌드하려면 npm run build를 실행해야 해요! 이는 서버가 연결되기 위한 아주 중요한 단계랍니다. 이제 기존 MCP 호스트인 Claude for Desktop에서 서버를 테스트해 볼게요.

Claude for Desktop으로 서버 테스트하기

먼저 Claude for Desktop이 설치되어 있어야 해요. 여기서 최신 버전을 설치할 수 있고, 이미 설치되어 있다면 최신 버전으로 업데이트했는지 확인해 주세요.

사용할 MCP 서버마다 Claude for Desktop을 설정해야 해요. Claude for Desktop 앱 설정 파일인 ~/Library/Application Support/Claude/claude_desktop_config.json을 텍스트 편집기로 열면 됩니다. 파일이 없으면 직접 만들어 주세요.

VS Code가 설치되어 있다면 이렇게 열 수 있어요.

Linux

code ~/.config/Claude/claude_desktop_config.json

macOS

code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows

code $env:AppData\Claude\claude_desktop_config.json

그다음 mcpServers 키에 서버를 추가하면 돼요. MCP UI 요소는 서버가 최소 하나 이상 제대로 설정되어 있을 때만 Claude for Desktop에 나타나요. 이번에는 날씨 서버 하나를 이렇게 추가해 볼게요.

macOS/Linux

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
    }
  }
}

Windows

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
    }
  }
}

이 설정이 Claude for Desktop에 알려주는 내용은 두 가지예요.

  1. "weather"라는 MCP 서버가 있다.
  2. node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js로 실행한다.

파일을 저장하고 Claude for Desktop을 다시 시작해 주세요.

Java

이 퀵스타트 데모는 Spring AI MCP 자동 구성과 boot starter를 기반으로 해요. 동기·비동기 MCP 서버를 수동으로 만드는 방법을 배우려면 Java SDK Server 문서를 참고하세요.

날씨 서버 만들기를 시작해 볼게요. 여기서 완성 코드를 볼 수 있어요. 자세한 내용은 MCP Server Boot Starter 참조 문서를 확인하세요. 수동 구현은 MCP Server Java SDK 문서를 참고하면 돼요.

MCP 서버에서의 로깅

MCP 서버를 구현할 때는 로깅을 어떻게 처리할지 조심해야 해요.

  • STDIO 기반 서버: System.out.println()이나 System.out.print()를 사용하면 안 됩니다. 기본적으로 표준 출력(stdout)에 쓰거든요. stdout에 쓰면 JSON-RPC 메시지가 깨지고 서버가 망가져요.
  • HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.

모범 사례

  • stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.
  • 설정한 로깅 라이브러리가 stdout에 쓰지 않는지 확인하세요.

시스템 요구 사항

환경 설정

Spring Initializer를 사용해 프로젝트를 부트스트랩하세요. 다음 의존성을 추가해야 합니다.

Maven

<dependencies>
      <dependency>
          <groupId>org.springframework.ai</groupId>
          <artifactId>spring-ai-starter-mcp-server</artifactId>
      </dependency>

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-web</artifactId>
      </dependency>
</dependencies>

Gradle

dependencies {
  implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
  implementation platform("org.springframework:spring-web")
}

그런 다음 애플리케이션 속성을 설정해 애플리케이션을 구성하세요.

application.properties

spring.main.bannerMode=off
logging.pattern.console=

application.yml

logging:
  pattern:
    console:
spring:
  main:
    banner-mode: off

Server Configuration Properties 문서에서 사용 가능한 모든 속성을 확인할 수 있어요. 이제 본격적으로 서버를 만들어 볼게요.

서버 만들기

Weather Service

REST 클라이언트로 국립기상청 API의 데이터를 조회하는 WeatherService.java를 구현해 볼게요.

@Service
public class WeatherService {

    private final RestClient restClient;

    public WeatherService() {
        this.restClient = RestClient.builder()
            .baseUrl("https://api.weather.gov")
            .defaultHeader("Accept", "application/geo+json")
            .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
            .build();
    }

  @Tool(description = "Get weather forecast for a specific latitude/longitude")
  public String getWeatherForecastByLocation(
      double latitude,   // Latitude coordinate
      double longitude   // Longitude coordinate
  ) {
      // Returns detailed forecast including:
      // - Temperature and unit
      // - Wind speed and direction
      // - Detailed forecast description
  }

  @Tool(description = "Get weather alerts for a US state")
  public String getAlerts(
      @ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state
  ) {
      // Returns active alerts including:
      // - Event type
      // - Affected area
      // - Severity
      // - Description
      // - Safety instructions
  }

  // ......
}

@Service 어노테이션은 서비스를 애플리케이션 컨텍스트에 자동 등록해 줍니다. Spring AI의 @Tool 어노테이션은 MCP 도구를 쉽게 만들고 유지하게 해 주고, 자동 구성이 이 도구들을 MCP 서버에 자동 등록해 줍니다.

Boot 애플리케이션 만들기

@SpringBootApplication
public class McpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(McpServerApplication.class, args);
    }

    @Bean
    public ToolCallbackProvider weatherTools(WeatherService weatherService) {
        return  MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
    }
}

MethodToolCallbackProvider 유틸을 사용해 @Tools를 MCP 서버가 사용하는 실행 가능한 콜백으로 변환합니다.

서버 실행

마지막으로 서버를 빌드해 볼게요.

./mvnw clean install

target 폴더 안에 mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar 파일이 생성됩니다. 이제 기존 MCP 호스트인 Claude for Desktop에서 서버를 테스트해 볼게요.

Claude for Desktop으로 서버 테스트하기

먼저 Claude for Desktop이 설치되어 있어야 해요. 여기서 최신 버전을 설치할 수 있고, 이미 설치되어 있다면 최신 버전으로 업데이트했는지 확인해 주세요.

사용할 MCP 서버마다 Claude for Desktop을 설정해야 해요. Claude for Desktop 앱 설정 파일인 ~/Library/Application Support/Claude/claude_desktop_config.json을 텍스트 편집기로 열면 됩니다. 파일이 없으면 직접 만들어 주세요.

VS Code가 설치되어 있다면 이렇게 열 수 있어요.

Linux

code ~/.config/Claude/claude_desktop_config.json

macOS

code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows

code $env:AppData\Claude\claude_desktop_config.json

그다음 mcpServers 키에 서버를 추가하면 돼요. MCP UI 요소는 서버가 최소 하나 이상 제대로 설정되어 있을 때만 Claude for Desktop에 나타나요. 이번에는 날씨 서버 하나를 이렇게 추가해 볼게요.

macOS/Linux

{
  "mcpServers": {
    "spring-ai-mcp-weather": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-jar",
        "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
      ]
    }
  }
}

Windows

{
  "mcpServers": {
    "spring-ai-mcp-weather": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.transport=STDIO",
        "-jar",
        "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
      ]
    }
  }
}

서버에는 반드시 절대 경로를 전달해 주세요.

이 설정이 Claude for Desktop에 알려주는 내용은 두 가지예요.

  1. "my-weather-server"라는 MCP 서버가 있다.
  2. java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar로 실행한다.

파일을 저장하고 Claude for Desktop을 다시 시작해 주세요.

Java 클라이언트로 서버 테스트하기

MCP 클라이언트 직접 만들기

McpClient를 사용해 서버에 연결할 수 있어요.

var stdioParams = ServerParameters.builder("java")
  .args("-jar", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar")
  .build();

var stdioTransport = new StdioClientTransport(stdioParams);

var mcpClient = McpClient.sync(stdioTransport).build();

mcpClient.initialize();

ListToolsResult toolsList = mcpClient.listTools();

CallToolResult weather = mcpClient.callTool(
  new CallToolRequest("getWeatherForecastByLocation",
      Map.of("latitude", "47.6062", "longitude", "-122.3321")));

CallToolResult alert = mcpClient.callTool(
  new CallToolRequest("getAlerts", Map.of("state", "NY")));

mcpClient.closeGracefully();

MCP Client Boot Starter 사용하기

spring-ai-starter-mcp-client 의존성을 사용해 새 boot starter 애플리케이션을 만들어 주세요.

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

그다음 spring.ai.mcp.client.stdio.servers-configuration 속성을 claude_desktop_config.json을 가리키도록 설정합니다. 기존 Anthropic Desktop 구성을 재사용할 수 있어요.

spring.ai.mcp.client.stdio.servers-configuration=file:PATH/TO/claude_desktop_config.json

클라이언트 애플리케이션을 시작하면 자동 구성이 claude_desktop_config.json에서 MCP 클라이언트를 자동으로 만들어 줍니다. 자세한 내용은 MCP Client Boot Starters 참조 문서를 확인하세요.

더 많은 Java MCP 서버 예시

starter-webflux-server는 WebFlux starter로 HTTP 기반 MCP 서버를 만드는 방법을 보여줘요. spring.ai.mcp.server.protocol=STREAMABLE 속성을 설정하면 Streamable HTTP로 서비스를 제공할 수 있고, Spring Boot의 자동 구성 기능으로 MCP Tools, Resources, Prompts를 정의하고 등록하는 방법을 보여줍니다.

Kotlin

날씨 서버 만들기를 시작해 볼게요. 여기서 완성 코드를 볼 수 있어요.

사전 지식

이 퀵스타트는 다음에 익숙하다고 가정해요.

  • Kotlin
  • Claude 같은 LLM

MCP 서버에서의 로깅

MCP 서버를 구현할 때는 로깅을 어떻게 처리할지 조심해야 해요.

  • STDIO 기반 서버: println()을 사용하면 안 됩니다. 기본적으로 표준 출력(stdout)에 쓰거든요. stdout에 쓰면 JSON-RPC 메시지가 깨지고 서버가 망가져요.
  • HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.

모범 사례

  • stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.

시스템 요구 사항

  • JDK 11 이상이 설치되어 있어야 해요.

환경 설정

먼저 javagradle을 아직 설치하지 않았다면 설치해 주세요. 공식 Oracle JDK 웹사이트에서 java를 다운로드할 수 있습니다. java 설치를 확인해 볼게요.

java --version

이제 프로젝트를 만들고 설정해 볼게요.

macOS/Linux

# Create a new directory for our project
mkdir weather
cd weather

# Initialize a new kotlin project
gradle init

Windows

# Create a new directory for our project
md weather
cd weather

# Initialize a new kotlin project
gradle init

gradle init 실행 후 프로젝트 유형으로 Application, 프로그래밍 언어로 Kotlin을 선택하세요. 또는 IntelliJ IDEA 프로젝트 마법사로 Kotlin 애플리케이션을 만들 수도 있어요. 프로젝트를 만든 뒤 build.gradle.kts 내용을 다음으로 교체하세요.

build.gradle.kts

// Check latest versions at https://github.com/modelcontextprotocol/kotlin-sdk/releases
val mcpVersion = "0.9.0"
val ktorVersion = "3.2.3"
val slf4jVersion = "2.0.17"

plugins {
    kotlin("jvm") version "2.3.20"
    kotlin("plugin.serialization") version "2.3.20"
    id("com.gradleup.shadow") version "8.3.9"
    application
}

application {
    mainClass.set("MainKt")
}

dependencies {
    implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
    implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
    implementation("io.ktor:ktor-client-cio:$ktorVersion")
    implementation("org.slf4j:slf4j-simple:$slf4jVersion")
}

모든 것이 제대로 설정됐는지 확인해 볼게요.

./gradlew build

이제 본격적으로 서버를 만들어 볼게요.

서버 만들기

인스턴스 설정

서버 초기화 함수를 추가해 주세요.

fun runMcpServer() {
    val server = Server(
        Implementation(
            name = "weather",
            version = "1.0.0",
        ),
        ServerOptions(
            capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
        ),
    )

    // register tools on server here

    val transport = StdioServerTransport(
        System.`in`.asInput(),
        System.out.asSink().buffered(),
    )

    runBlocking {
        val session = server.createSession(transport)
        val done = Job()
        session.onClose {
            done.complete()
        }
        done.join()
    }
}

날씨 API 헬퍼 함수

다음으로 국립기상청 API에서 응답을 조회하고 변환하는 함수와 데이터 클래스를 추가할게요.

val httpClient = HttpClient(CIO) {
    defaultRequest {
        url("https://api.weather.gov")
        headers {
            append("Accept", "application/geo+json")
            append("User-Agent", "WeatherApiClient/1.0")
        }
        contentType(ContentType.Application.Json)
    }
    install(ContentNegotiation) {
        json(Json { ignoreUnknownKeys = true })
    }
}

// Extension function to fetch weather alerts for a given state
suspend fun HttpClient.getAlerts(state: String): List<String> {
    val alerts = this.get("/alerts/active/area/$state").body<AlertsResponse>()
    return alerts.features.map { feature ->
        """
            Event: ${feature.properties.event}
            Area: ${feature.properties.areaDesc}
            Severity: ${feature.properties.severity}
            Status: ${feature.properties.status}
            Headline: ${feature.properties.headline}
        """.trimIndent()
    }
}

// Extension function to fetch forecast information for given latitude and longitude
suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List<String> {
    val points = this.get("/points/$latitude,$longitude").body<PointsResponse>()
    val forecastUrl = points.properties.forecast ?: error("No forecast URL available")
    val forecast = this.get(forecastUrl).body<ForecastResponse>()
    return forecast.properties.periods.map { period ->
        """
            ${period.name}:
            Temperature: ${period.temperature}°${period.temperatureUnit}
            Wind: ${period.windSpeed} ${period.windDirection}
            ${period.shortForecast}
        """.trimIndent()
    }
}

@Serializable
data class PointsResponse(val properties: PointsProperties)

@Serializable
data class PointsProperties(val forecast: String? = null)

@Serializable
data class ForecastResponse(val properties: ForecastProperties)

@Serializable
data class ForecastProperties(val periods: List<ForecastPeriod> = emptyList())

@Serializable
data class ForecastPeriod(
    val name: String? = null,
    val temperature: Int? = null,
    val temperatureUnit: String? = null,
    val windSpeed: String? = null,
    val windDirection: String? = null,
    val shortForecast: String? = null,
)

@Serializable
data class AlertsResponse(val features: List<AlertFeature> = emptyList())

@Serializable
data class AlertFeature(val properties: AlertProperties)

@Serializable
data class AlertProperties(
    val event: String? = null,
    val areaDesc: String? = null,
    val severity: String? = null,
    val status: String? = null,
    val headline: String? = null,
)

도구 실행 구현

도구 실행 핸들러는 각 도구의 실제 로직을 실행하는 역할을 해요. 추가해 볼게요.

// Register weather tools

server.addTool(
    name = "get_alerts",
    description = "Get weather alerts for a US state. Input is a two-letter US state code (e.g. CA, NY)",
    inputSchema = ToolSchema(
        properties = buildJsonObject {
            putJsonObject("state") {
                put("type", "string")
                put("description", "Two-letter US state code (e.g. CA, NY)")
            }
        },
        required = listOf("state"),
    ),
) { request ->
    val state = request.arguments?.get("state")?.jsonPrimitive?.content
        ?: return@addTool CallToolResult(
            content = listOf(TextContent("The 'state' parameter is required.")),
        )

    val alerts = httpClient.getAlerts(state)
    CallToolResult(content = alerts.map { TextContent(it) })
}

server.addTool(
    name = "get_forecast",
    description = "Get weather forecast for a location. Note: only US locations are supported by the NWS API.",
    inputSchema = ToolSchema(
        properties = buildJsonObject {
            putJsonObject("latitude") {
                put("type", "number")
                put("description", "Latitude of the location")
            }
            putJsonObject("longitude") {
                put("type", "number")
                put("description", "Longitude of the location")
            }
        },
        required = listOf("latitude", "longitude"),
    ),
) { request ->
    val latitude = request.arguments?.get("latitude")?.jsonPrimitive?.doubleOrNull
    val longitude = request.arguments?.get("longitude")?.jsonPrimitive?.doubleOrNull
    if (latitude == null || longitude == null) {
        return@addTool CallToolResult(
            content = listOf(TextContent("The 'latitude' and 'longitude' parameters are required.")),
        )
    }

    val forecast = httpClient.getForecast(latitude, longitude)
    CallToolResult(content = forecast.map { TextContent(it) })
}

서버 실행

마지막으로 서버를 실행하는 main 함수를 구현합니다.

fun main() = runMcpServer()

개발 중에는 서버를 직접 실행할 수 있어요.

./gradlew run

프로덕션에서는 shadow JAR을 빌드하세요.

./gradlew build
java -jar build/libs/weather-0.1.0-all.jar

이제 기존 MCP 호스트인 Claude for Desktop에서 서버를 테스트해 볼게요.

Claude for Desktop으로 서버 테스트하기

먼저 Claude for Desktop이 설치되어 있어야 해요. 여기서 최신 버전을 설치할 수 있고, 이미 설치되어 있다면 최신 버전으로 업데이트했는지 확인해 주세요.

사용할 MCP 서버마다 Claude for Desktop을 설정해야 해요. Claude for Desktop 앱 설정 파일인 ~/Library/Application Support/Claude/claude_desktop_config.json을 텍스트 편집기로 열면 됩니다. 파일이 없으면 직접 만들어 주세요.

VS Code가 설치되어 있다면 이렇게 열 수 있어요.

Linux

code ~/.config/Claude/claude_desktop_config.json

macOS

code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows

code $env:AppData\Claude\claude_desktop_config.json

그다음 mcpServers 키에 서버를 추가하면 돼요. MCP UI 요소는 서버가 최소 하나 이상 제대로 설정되어 있을 때만 Claude for Desktop에 나타나요. 이번에는 날씨 서버 하나를 이렇게 추가해 볼게요.

macOS/Linux

{
  "mcpServers": {
    "weather": {
      "command": "java",
      "args": [
        "-jar",
        "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
      ]
    }
  }
}

Windows

{
  "mcpServers": {
    "weather": {
      "command": "java",
      "args": [
        "-jar",
        "C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
      ]
    }
  }
}

이 설정이 Claude for Desktop에 알려주는 내용은 두 가지예요.

  1. "weather"라는 MCP 서버가 있다.
  2. java -jar /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar로 실행한다.

파일을 저장하고 Claude for Desktop을 다시 시작해 주세요.

C

날씨 서버 만들기를 시작해 볼게요. 여기서 완성 코드를 볼 수 있어요.

사전 지식

이 퀵스타트는 다음에 익숙하다고 가정해요.

  • C#
  • Claude 같은 LLM
  • .NET 8 이상

MCP 서버에서의 로깅

MCP 서버를 구현할 때는 로깅을 어떻게 처리할지 조심해야 해요.

  • STDIO 기반 서버: Console.WriteLine()이나 Console.Write()를 사용하면 안 됩니다. 기본적으로 표준 출력(stdout)에 쓰거든요. stdout에 쓰면 JSON-RPC 메시지가 깨지고 서버가 망가져요.
  • HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.

모범 사례

  • stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.

시스템 요구 사항

  • .NET 8 SDK 이상이 설치되어 있어야 해요.

환경 설정

먼저 dotnet을 아직 설치하지 않았다면 설치해 주세요. 공식 Microsoft .NET 웹사이트에서 다운로드할 수 있습니다. dotnet 설치를 확인해 볼게요.

dotnet --version

이제 프로젝트를 만들고 설정해 볼게요.

macOS/Linux

# Create a new directory for our project
mkdir weather
cd weather
# Initialize a new C# project
dotnet new console

Windows

# Create a new directory for our project
mkdir weather
cd weather
# Initialize a new C# project
dotnet new console

dotnet new console를 실행하면 새 C# 프로젝트가 만들어져요. Visual StudioRider 같은 선호하는 IDE에서 프로젝트를 열 수 있습니다. 또는 Visual Studio 프로젝트 마법사로 C# 애플리케이션을 만들 수도 있어요. 프로젝트를 만든 뒤 Model Context Protocol SDK와 호스팅용 NuGet 패키지를 추가해 주세요.

# Add the Model Context Protocol SDK NuGet package
dotnet add package ModelContextProtocol --prerelease
# Add the .NET Hosting NuGet package
dotnet add package Microsoft.Extensions.Hosting

이제 본격적으로 서버를 만들어 볼게요.

서버 만들기

프로젝트의 Program.cs 파일을 열고 내용을 다음 코드로 교체하세요.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol;
using System.Net.Http.Headers;

var builder = Host.CreateEmptyApplicationBuilder(settings: null);

builder.Services.AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

builder.Services.AddSingleton(_ =>
{
    var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
    client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
    return client;
});

var app = builder.Build();

await app.RunAsync();

ApplicationHostBuilder를 만들 때는 CreateDefaultBuilder 대신 CreateEmptyApplicationBuilder를 사용해야 해요. 그래야 서버가 콘솔에 추가 메시지를 쓰지 않습니다. 이는 STDIO transport를 사용하는 서버에서만 필요한 부분이에요.

이 코드는 Model Context Protocol SDK를 사용해 표준 I/O transport로 MCP 서버를 만드는 기본 콘솔 애플리케이션을 구성합니다.

날씨 API 헬퍼 함수

JSON 요청 처리를 단순화하는 HttpClient용 확장 클래스를 만들어 주세요.

using System.Text.Json;

internal static class HttpClientExt
{
    public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
    {
        using var response = await client.GetAsync(requestUri);
        response.EnsureSuccessStatusCode();
        return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
    }
}

다음으로 국립기상청 API에서 응답을 조회하고 변환하는 도구 실행 핸들러가 있는 클래스를 정의해 볼게요.

using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Globalization;
using System.Text.Json;

namespace QuickstartWeatherServer.Tools;

[McpServerToolType]
public static class WeatherTools
{
    [McpServerTool, Description("Get weather alerts for a US state code.")]
    public static async Task<string> GetAlerts(
        HttpClient client,
        [Description("The US state code to get alerts for.")] string state)
    {
        using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
        var jsonElement = jsonDocument.RootElement;
        var alerts = jsonElement.GetProperty("features").EnumerateArray();

        if (!alerts.Any())
        {
            return "No active alerts for this state.";
        }

        return string.Join("\n--\n", alerts.Select(alert =>
        {
            JsonElement properties = alert.GetProperty("properties");
            return $"""
                    Event: {properties.GetProperty("event").GetString()}
                    Area: {properties.GetProperty("areaDesc").GetString()}
                    Severity: {properties.GetProperty("severity").GetString()}
                    Description: {properties.GetProperty("description").GetString()}
                    Instruction: {properties.GetProperty("instruction").GetString()}
                    """;
        }));
    }

    [McpServerTool, Description("Get weather forecast for a location.")]
    public static async Task<string> GetForecast(
        HttpClient client,
        [Description("Latitude of the location.")] double latitude,
        [Description("Longitude of the location.")] double longitude)
    {
        var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
        using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
        var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
            ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");

        using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
        var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();

        return string.Join("\n---\n", periods.Select(period => $"""
                {period.GetProperty("name").GetString()}
                Temperature: {period.GetProperty("temperature").GetInt32()}°F
                Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
                Forecast: {period.GetProperty("detailedForecast").GetString()}
                """));
    }
}

서버 실행

마지막으로 다음 명령으로 서버를 실행합니다.

dotnet run

이렇게 하면 서버가 시작되고 표준 입력/출력에서 들어오는 요청을 기다리게 됩니다.

Claude for Desktop으로 서버 테스트하기

먼저 Claude for Desktop이 설치되어 있어야 해요. 여기서 최신 버전을 설치할 수 있고, 이미 설치되어 있다면 최신 버전으로 업데이트했는지 확인해 주세요.

사용할 MCP 서버마다 Claude for Desktop을 설정해야 해요. Claude for Desktop 앱 설정 파일인 ~/Library/Application Support/Claude/claude_desktop_config.json을 텍스트 편집기로 열면 됩니다. 파일이 없으면 직접 만들어 주세요.

VS Code가 설치되어 있다면 이렇게 열 수 있어요.

Linux

code ~/.config/Claude/claude_desktop_config.json

macOS

code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows

code $env:AppData\Claude\claude_desktop_config.json

그다음 mcpServers 키에 서버를 추가하면 돼요. MCP UI 요소는 서버가 최소 하나 이상 제대로 설정되어 있을 때만 Claude for Desktop에 나타나요. 이번에는 날씨 서버 하나를 이렇게 추가해 볼게요.

macOS/Linux

{
  "mcpServers": {
    "weather": {
      "command": "dotnet",
      "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
    }
  }
}

Windows

{
  "mcpServers": {
    "weather": {
      "command": "dotnet",
      "args": [
        "run",
        "--project",
        "C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
        "--no-build"
      ]
    }
  }
}

이 설정이 Claude for Desktop에 알려주는 내용은 두 가지예요.

  1. "weather"라는 MCP 서버가 있다.
  2. dotnet run /ABSOLUTE/PATH/TO/PROJECT로 실행한다.

파일을 저장하고 Claude for Desktop을 다시 시작해 주세요.

Ruby

날씨 서버 만들기를 시작해 볼게요. 여기서 완성 코드를 볼 수 있어요.

사전 지식

이 퀵스타트는 다음에 익숙하다고 가정해요.

  • Ruby
  • Claude 같은 LLM

MCP 서버에서의 로깅

MCP 서버를 구현할 때는 로깅을 어떻게 처리할지 조심해야 해요.

  • STDIO 기반 서버: putsprint를 사용하면 안 됩니다. 기본적으로 표준 출력(stdout)에 쓰거든요. stdout에 쓰면 JSON-RPC 메시지가 깨지고 서버가 망가져요.
  • HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.

모범 사례

  • stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.

빠른 예시

# ❌ 나쁜 예 (STDIO)
puts "Processing request"

# ✅ 좋은 예 (STDIO)
require "logger"
logger = Logger.new($stderr)
logger.info("Processing request")

시스템 요구 사항

  • Ruby 2.7 이상이 설치되어 있어야 해요.

환경 설정

먼저 Ruby가 설치되어 있는지 확인해 주세요. 다음 명령으로 확인할 수 있어요.

ruby --version

이제 프로젝트를 만들고 설정해 볼게요.

macOS/Linux

# Create a new directory for our project
mkdir weather
cd weather

# Create a Gemfile
bundle init

# Add the MCP SDK dependency
bundle add mcp

# Create our server file
touch weather.rb

Windows

# Create a new directory for our project
mkdir weather
cd weather

# Create a Gemfile
bundle init

# Add the MCP SDK dependency
bundle add mcp

# Create our server file
new-item weather.rb

이제 본격적으로 서버를 만들어 볼게요.

서버 만들기

패키지 임포트와 상수 설정

weather.rb를 열고 맨 위에 다음 require와 상수를 추가해 주세요.

require "json"
require "mcp"
require "net/http"
require "uri"

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

mcp gem은 Ruby용 Model Context Protocol SDK를 제공하는데, 서버 구현과 stdio transport용 클래스를 포함해요.

헬퍼 메서드

다음으로 국립기상청 API에서 데이터를 조회하고 포맷하는 헬퍼 메서드를 추가할게요.

module HelperMethods
  def make_nws_request(url)
    uri = URI(url)
    request = Net::HTTP::Get.new(uri)
    request["User-Agent"] = USER_AGENT
    request["Accept"] = "application/geo+json"

    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
      http.request(request)
    end

    raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)

    JSON.parse(response.body)
  end

  def format_alert(feature)
    properties = feature["properties"]

    <<~ALERT
      Event: #{properties["event"] || "Unknown"}
      Area: #{properties["areaDesc"] || "Unknown"}
      Severity: #{properties["severity"] || "Unknown"}
      Description: #{properties["description"] || "No description available"}
      Instructions: #{properties["instruction"] || "No specific instructions provided"}
    ALERT
  end
end

도구 실행 구현

이제 도구 클래스를 정의해 볼게요. 각 도구는 MCP::Tool을 상속하고 도구 로직을 구현합니다.

class GetAlerts < MCP::Tool
  extend HelperMethods

  tool_name "get_alerts"
  description "Get weather alerts for a US state"
  input_schema(
    properties: {
      state: {
        type: "string",
        description: "Two-letter US state code (e.g. CA, NY)"
      }
    },
    required: ["state"]
  )

  def self.call(state:)
    url = "#{NWS_API_BASE}/alerts/active/area/#{state.upcase}"
    data = make_nws_request(url)

    if data["features"].empty?
      return MCP::Tool::Response.new([{
        type: "text",
        text: "No active alerts for this state."
      }])
    end

    alerts = data["features"].map { |feature| format_alert(feature) }
    MCP::Tool::Response.new([{
      type: "text",
      text: alerts.join("\n---\n")
    }])
  end
end

class GetForecast < MCP::Tool
  extend HelperMethods

  tool_name "get_forecast"
  description "Get weather forecast for a location"
  input_schema(
    properties: {
      latitude: {
        type: "number",
        description: "Latitude of the location"
      },
      longitude: {
        type: "number",
        description: "Longitude of the location"
      }
    },
    required: ["latitude", "longitude"]
  )

  def self.call(latitude:, longitude:)
    # First get the forecast grid endpoint.
    points_url = "#{NWS_API_BASE}/points/#{latitude},#{longitude}"
    points_data = make_nws_request(points_url)

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

    # Format the periods into a readable forecast.
    periods = forecast_data["properties"]["periods"]
    forecasts = periods.first(5).map do |period|
      <<~FORECAST
        #{period["name"]}:
        Temperature: #{period["temperature"]}°#{period["temperatureUnit"]}
        Wind: #{period["windSpeed"]} #{period["windDirection"]}
        Forecast: #{period["detailedForecast"]}
      FORECAST
    end

    MCP::Tool::Response.new([{
      type: "text",
      text: forecasts.join("\n---\n")
    }])
  end
end

서버 실행

마지막으로 서버를 초기화하고 실행합니다.

server = MCP::Server.new(
  name: "weather",
  version: "1.0.0",
  tools: [GetAlerts, GetForecast]
)

transport = MCP::Server::Transports::StdioTransport.new(server)
transport.open

서버가 완성됐어요! bundle exec ruby weather.rb로 MCP 서버를 시작하면, MCP 호스트가 보내는 메시지를 기다리기 시작해요. 이제 기존 MCP 호스트인 Claude for Desktop에서 서버를 테스트해 볼게요.

Claude for Desktop으로 서버 테스트하기

먼저 Claude for Desktop이 설치되어 있어야 해요. 여기서 최신 버전을 설치할 수 있고, 이미 설치되어 있다면 최신 버전으로 업데이트했는지 확인해 주세요.

사용할 MCP 서버마다 Claude for Desktop을 설정해야 해요. Claude for Desktop 앱 설정 파일인 ~/Library/Application Support/Claude/claude_desktop_config.json을 텍스트 편집기로 열면 됩니다. 파일이 없으면 직접 만들어 주세요.

VS Code가 설치되어 있다면 이렇게 열 수 있어요.

Linux

code ~/.config/Claude/claude_desktop_config.json

macOS

code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows

code $env:AppData\Claude\claude_desktop_config.json

그다음 mcpServers 키에 서버를 추가하면 돼요. MCP UI 요소는 서버가 최소 하나 이상 제대로 설정되어 있을 때만 Claude for Desktop에 나타나요. 이번에는 날씨 서버 하나를 이렇게 추가해 볼게요.

macOS/Linux

{
  "mcpServers": {
    "weather": {
      "command": "bundle",
      "args": ["exec", "ruby", "weather.rb"],
      "cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
    }
  }
}

Windows

{
  "mcpServers": {
    "weather": {
      "command": "bundle",
      "args": ["exec", "ruby", "weather.rb"],
      "cwd": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather"
    }
  }
}

cwd 필드에는 프로젝트 디렉터리의 절대 경로를 전달해 주세요. 프로젝트 디렉터리에서 macOS/Linux에서는 pwd, Windows 명령 프롬프트에서는 cd로 확인할 수 있어요. Windows에서는 JSON 경로에 이중 백슬래시(\\)나 슬래시(/)를 사용해야 합니다.

이 설정이 Claude for Desktop에 알려주는 내용은 두 가지예요.

  1. "weather"라는 MCP 서버가 있다.
  2. 지정된 디렉터리에서 bundle exec ruby weather.rb로 실행한다.

파일을 저장하고 Claude for Desktop을 다시 시작해 주세요.

Rust

날씨 서버 만들기를 시작해 볼게요. 여기서 완성 코드를 볼 수 있어요.

사전 지식

이 퀵스타트는 다음에 익숙하다고 가정해요.

  • Rust 프로그래밍 언어
  • Rust에서의 async/await
  • Claude 같은 LLM

MCP 서버에서의 로깅

MCP 서버를 구현할 때는 로깅을 어떻게 처리할지 조심해야 해요.

  • STDIO 기반 서버: println!()이나 print!()를 사용하면 안 됩니다. 기본적으로 표준 출력(stdout)에 쓰거든요. stdout에 쓰면 JSON-RPC 메시지가 깨지고 서버가 망가져요.
  • HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.

모범 사례

  • Rust에서는 tracing이나 log처럼 stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.
  • 로깅 프레임워크가 stdout 출력을 피하도록 설정하세요.

빠른 예시

// ❌ 나쁜 예 (STDIO)
println!("Processing request");

// ✅ 좋은 예 (STDIO)
eprintln!("Processing request"); // writes to stderr

시스템 요구 사항

  • Rust 1.70 이상이 설치되어 있어야 해요.
  • Cargo (Rust 설치 시 함께 딸려 옵니다).

환경 설정

먼저 Rust를 아직 설치하지 않았다면 설치해 주세요. rust-lang.org에서 Rust를 설치할 수 있습니다.

macOS/Linux

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Windows

# Download and run rustup-init.exe from https://rustup.rs/

Rust 설치를 확인해 볼게요.

rustc --version
cargo --version

이제 프로젝트를 만들고 설정해 볼게요.

macOS/Linux

# Create a new Rust project
cargo new weather
cd weather

Windows

# Create a new Rust project
cargo new weather
cd weather

Cargo.toml을 업데이트해 필요한 의존성을 추가해 주세요.

Cargo.toml

[package]
name = "weather"
version = "0.1.0"
edition = "2024"

[dependencies]
rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
tokio = { version = "1.46", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }

이제 본격적으로 서버를 만들어 볼게요.

서버 만들기

패키지 임포트와 상수

src/main.rs를 열고 맨 위에 다음 임포트와 상수를 추가해 주세요.

use anyhow::Result;
use rmcp::{
    ServerHandler, ServiceExt,
    handler::server::{router::tool::ToolRouter, tool::Parameters},
    model::*,
    schemars, tool, tool_handler, tool_router,
};
use serde::Deserialize;
use serde::de::DeserializeOwned;

const NWS_API_BASE: &str = "https://api.weather.gov";
const USER_AGENT: &str = "weather-app/1.0";

rmcp 크레이트는 Rust용 Model Context Protocol SDK를 제공하는데, 서버 구현, 절차적 매크로, stdio transport를 위한 기능을 포함해요.

데이터 구조

다음으로 국립기상청 API에서 응답을 역직렬화할 데이터 구조를 정의해 볼게요.

#[derive(Debug, Deserialize)]
struct AlertsResponse {
    features: Vec<AlertFeature>,
}

#[derive(Debug, Deserialize)]
struct AlertFeature {
    properties: AlertProperties,
}

#[derive(Debug, Deserialize)]
struct AlertProperties {
    event: Option<String>,
    #[serde(rename = "areaDesc")]
    area_desc: Option<String>,
    severity: Option<String>,
    description: Option<String>,
    instruction: Option<String>,
}

#[derive(Debug, Deserialize)]
struct PointsResponse {
    properties: PointsProperties,
}

#[derive(Debug, Deserialize)]
struct PointsProperties {
    forecast: String,
}

#[derive(Debug, Deserialize)]
struct ForecastResponse {
    properties: ForecastProperties,
}

#[derive(Debug, Deserialize)]
struct ForecastProperties {
    periods: Vec<ForecastPeriod>,
}

#[derive(Debug, Deserialize)]
struct ForecastPeriod {
    name: String,
    temperature: i32,
    #[serde(rename = "temperatureUnit")]
    temperature_unit: String,
    #[serde(rename = "windSpeed")]
    wind_speed: String,
    #[serde(rename = "windDirection")]
    wind_direction: String,
    #[serde(rename = "detailedForecast")]
    detailed_forecast: String,
}

이제 MCP 클라이언트가 보낼 요청 타입을 정의해 볼게요.

#[derive(serde::Deserialize, schemars::JsonSchema)]
pub struct MCPForecastRequest {
    latitude: f32,
    longitude: f32,
}

#[derive(serde::Deserialize, schemars::JsonSchema)]
pub struct MCPAlertRequest {
    state: String,
}

헬퍼 함수

API 요청을 보내고 응답을 포맷하는 헬퍼 함수를 추가해 주세요.

async fn make_nws_request<T: DeserializeOwned>(url: &str) -> Result<T> {
    let client = reqwest::Client::new();
    let rsp = client
        .get(url)
        .header(reqwest::header::USER_AGENT, USER_AGENT)
        .header(reqwest::header::ACCEPT, "application/geo+json")
        .send()
        .await?
        .error_for_status()?;
    Ok(rsp.json::<T>().await?)
}

fn format_alert(feature: &AlertFeature) -> String {
    let props = &feature.properties;
    format!(
        "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}",
        props.event.as_deref().unwrap_or("Unknown"),
        props.area_desc.as_deref().unwrap_or("Unknown"),
        props.severity.as_deref().unwrap_or("Unknown"),
        props
            .description
            .as_deref()
            .unwrap_or("No description available"),
        props
            .instruction
            .as_deref()
            .unwrap_or("No specific instructions provided")
    )
}

fn format_period(period: &ForecastPeriod) -> String {
    format!(
        "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}",
        period.name,
        period.temperature,
        period.temperature_unit,
        period.wind_speed,
        period.wind_direction,
        period.detailed_forecast
    )
}

Weather 서버와 도구 구현

이제 도구 핸들러가 있는 메인 Weather 서버 구조체를 구현해 볼게요.

pub struct Weather {
    tool_router: ToolRouter<Weather>,
}

#[tool_router]
impl Weather {
    fn new() -> Self {
        Self {
            tool_router: Self::tool_router(),
        }
    }

    #[tool(description = "Get weather alerts for a US state.")]
    async fn get_alerts(
        &self,
        Parameters(MCPAlertRequest { state }): Parameters<MCPAlertRequest>,
    ) -> String {
        let url = format!(
            "{}/alerts/active/area/{}",
            NWS_API_BASE,
            state.to_uppercase()
        );

        match make_nws_request::<AlertsResponse>(&url).await {
            Ok(data) => {
                if data.features.is_empty() {
                    "No active alerts for this state.".to_string()
                } else {
                    data.features
                        .iter()
                        .map(format_alert)
                        .collect::<Vec<_>>()
                        .join("\n---\n")
                }
            }
            Err(_) => "Unable to fetch alerts or no alerts found.".to_string(),
        }
    }

    #[tool(description = "Get weather forecast for a location.")]
    async fn get_forecast(
        &self,
        Parameters(MCPForecastRequest {
            latitude,
            longitude,
        }): Parameters<MCPForecastRequest>,
    ) -> String {
        let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
        let Ok(points_data) = make_nws_request::<PointsResponse>(&points_url).await else {
            return "Unable to fetch forecast data for this location.".to_string();
        };

        let forecast_url = points_data.properties.forecast;

        let Ok(forecast_data) = make_nws_request::<ForecastResponse>(&forecast_url).await else {
            return "Unable to fetch forecast data for this location.".to_string();
        };

        let periods = &forecast_data.properties.periods;
        let forecast_summary: String = periods
            .iter()
            .take(5) // Next 5 periods only
            .map(format_period)
            .collect::<Vec<String>>()
            .join("\n---\n");
        forecast_summary
    }
}

#[tool_router] 매크로는 라우팅 로직을 자동 생성하고, #[tool] 어트리뷰트는 메서드를 MCP 도구로 표시해 줍니다.

ServerHandler 구현

서버 능력을 정의하기 위해 ServerHandler 트레이트를 구현해 주세요.

#[tool_handler]
impl ServerHandler for Weather {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            capabilities: ServerCapabilities::builder().enable_tools().build(),
            ..Default::default()
        }
    }
}

서버 실행

마지막으로 stdio transport로 서버를 실행하는 main 함수를 구현합니다.

#[tokio::main]
async fn main() -> Result<()> {
    let transport = (tokio::io::stdin(), tokio::io::stdout());
    let service = Weather::new().serve(transport).await?;
    service.waiting().await?;
    Ok(())
}

다음 명령으로 서버를 빌드하세요.

cargo build --release

컴파일된 바이너리는 target/release/weather에 있을 거예요. 이제 기존 MCP 호스트인 Claude for Desktop에서 서버를 테스트해 볼게요.

Claude for Desktop으로 서버 테스트하기

먼저 Claude for Desktop이 설치되어 있어야 해요. 여기서 최신 버전을 설치할 수 있고, 이미 설치되어 있다면 최신 버전으로 업데이트했는지 확인해 주세요.

사용할 MCP 서버마다 Claude for Desktop을 설정해야 해요. Claude for Desktop 앱 설정 파일인 ~/Library/Application Support/Claude/claude_desktop_config.json을 텍스트 편집기로 열면 됩니다. 파일이 없으면 직접 만들어 주세요.

VS Code가 설치되어 있다면 이렇게 열 수 있어요.

Linux

code ~/.config/Claude/claude_desktop_config.json

macOS

code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows

code $env:AppData\Claude\claude_desktop_config.json

그다음 mcpServers 키에 서버를 추가하면 돼요. MCP UI 요소는 서버가 최소 하나 이상 제대로 설정되어 있을 때만 Claude for Desktop에 나타나요. 이번에는 날씨 서버 하나를 이렇게 추가해 볼게요.

macOS/Linux

{
  "mcpServers": {
    "weather": {
      "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
    }
  }
}

Windows

{
  "mcpServers": {
    "weather": {
      "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\target\\release\\weather.exe"
    }
  }
}

컴파일된 바이너리의 절대 경로를 전달해 주세요. 프로젝트 디렉터리에서 macOS/Linux에서는 pwd, Windows 명령 프롬프트에서는 cd로 확인할 수 있어요. Windows에서는 JSON 경로에 이중 백슬래시(\\)나 슬래시(/)를 사용하고 .exe 확장자를 붙여야 합니다.

이 설정이 Claude for Desktop에 알려주는 내용은 두 가지예요.

  1. "weather"라는 MCP 서버가 있다.
  2. 지정된 경로의 컴파일된 바이너리로 실행한다.

파일을 저장하고 Claude for Desktop을 다시 시작해 주세요.

Go

날씨 서버 만들기를 시작해 볼게요. 여기서 완성 코드를 볼 수 있어요.

사전 지식

이 퀵스타트는 다음에 익숙하다고 가정해요.

  • Go
  • Claude 같은 LLM

MCP 서버에서의 로깅

MCP 서버를 구현할 때는 로깅을 어떻게 처리할지 조심해야 해요.

  • STDIO 기반 서버: fmt.Println()이나 fmt.Printf()를 사용하면 안 됩니다. 기본적으로 표준 출력(stdout)에 쓰거든요. stdout에 쓰면 JSON-RPC 메시지가 깨지고 서버가 망가져요.
  • HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.

모범 사례

  • (기본적으로 stderr로 가는) log.Println()이나 stderr·파일에 쓰는 로깅 라이브러리를 사용하세요.
  • fmt.Fprintf(os.Stderr, ...)로 명시적으로 stderr에 쓰세요.

빠른 예시

// ❌ 나쁜 예 (STDIO)
fmt.Println("Processing request")

// ✅ 좋은 예 (STDIO)
log.Println("Processing request") // defaults to stderr

// ✅ 좋은 예 (STDIO)
fmt.Fprintln(os.Stderr, "Processing request")

시스템 요구 사항

  • Go 1.24 이상이 설치되어 있어야 해요.

환경 설정

먼저 Go를 아직 설치하지 않았다면 설치해 주세요. go.dev에서 Go를 다운로드해 설치할 수 있습니다. Go 설치를 확인해 볼게요.

go version

이제 프로젝트를 만들고 설정해 볼게요.

macOS/Linux

# Create a new directory for our project
mkdir weather
cd weather

# Initialize Go module
go mod init weather

# Install dependencies
go get github.com/modelcontextprotocol/go-sdk/mcp

# Create our server file
touch main.go

Windows

# Create a new directory for our project
md weather
cd weather

# Initialize Go module
go mod init weather

# Install dependencies
go get github.com/modelcontextprotocol/go-sdk/mcp

# Create our server file
new-item main.go

이제 본격적으로 서버를 만들어 볼게요.

서버 만들기

패키지 임포트와 상수

main.go 맨 위에 다음을 추가해 주세요.

package main

import (
    "cmp"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "strings"

    "github.com/modelcontextprotocol/go-sdk/mcp"
)

const (
    NWSAPIBase = "https://api.weather.gov"
    UserAgent  = "weather-app/1.0"
)

데이터 구조

다음으로 도구가 사용하는 데이터 구조를 정의해 볼게요.

type PointsResponse struct {
    Properties struct {
        Forecast string `json:"forecast"`
    } `json:"properties"`
}

type ForecastResponse struct {
    Properties struct {
        Periods []ForecastPeriod `json:"periods"`
    } `json:"properties"`
}

type ForecastPeriod struct {
    Name             string `json:"name"`
    Temperature      int    `json:"temperature"`
    TemperatureUnit  string `json:"temperatureUnit"`
    WindSpeed        string `json:"windSpeed"`
    WindDirection    string `json:"windDirection"`
    DetailedForecast string `json:"detailedForecast"`
}

type AlertsResponse struct {
    Features []AlertFeature `json:"features"`
}

type AlertFeature struct {
    Properties AlertProperties `json:"properties"`
}

type AlertProperties struct {
    Event       string `json:"event"`
    AreaDesc    string `json:"areaDesc"`
    Severity    string `json:"severity"`
    Description string `json:"description"`
    Instruction string `json:"instruction"`
}

type ForecastInput struct {
    Latitude  float64 `json:"latitude" jsonschema:"Latitude of the location"`
    Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
}

type AlertsInput struct {
    State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
}

헬퍼 함수

다음으로 국립기상청 API에서 데이터를 조회하고 포맷하는 헬퍼 함수를 추가할게요.

func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, fmt.Errorf("failed to create request: %w", err)
    }

    req.Header.Set("User-Agent", UserAgent)
    req.Header.Set("Accept", "application/geo+json")

    client := http.DefaultClient
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(resp.Body)
        return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
    }

    var result T
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("failed to decode response: %w", err)
    }

    return &result, nil
}

func formatAlert(alert AlertFeature) string {
    props := alert.Properties
    event := cmp.Or(props.Event, "Unknown")
    areaDesc := cmp.Or(props.AreaDesc, "Unknown")
    severity := cmp.Or(props.Severity, "Unknown")
    description := cmp.Or(props.Description, "No description available")
    instruction := cmp.Or(props.Instruction, "No specific instructions provided")

    return fmt.Sprintf(`
Event: %s
Area: %s
Severity: %s
Description: %s
Instructions: %s
`, event, areaDesc, severity, description, instruction)
}

func formatPeriod(period ForecastPeriod) string {
    return fmt.Sprintf(`
%s:
Temperature: %d°%s
Wind: %s %s
Forecast: %s
`, period.Name, period.Temperature, period.TemperatureUnit,
        period.WindSpeed, period.WindDirection, period.DetailedForecast)
}

도구 실행 구현

도구 실행 핸들러는 각 도구의 실제 로직을 실행하는 역할을 해요. 추가해 볼게요.

func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
    *mcp.CallToolResult, any, error,
) {
    // Get points data
    pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
    pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
    if err != nil {
        return &mcp.CallToolResult{
            Content: []mcp.Content{
                &mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
            },
        }, nil, nil
    }

    // Get forecast data
    forecastURL := pointsData.Properties.Forecast
    if forecastURL == "" {
        return &mcp.CallToolResult{
            Content: []mcp.Content{
                &mcp.TextContent{Text: "Unable to fetch forecast URL."},
            },
        }, nil, nil
    }

    forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
    if err != nil {
        return &mcp.CallToolResult{
            Content: []mcp.Content{
                &mcp.TextContent{Text: "Unable to fetch detailed forecast."},
            },
        }, nil, nil
    }

    // Format the periods
    periods := forecastData.Properties.Periods
    if len(periods) == 0 {
        return &mcp.CallToolResult{
            Content: []mcp.Content{
                &mcp.TextContent{Text: "No forecast periods available."},
            },
        }, nil, nil
    }

    // Show next 5 periods
    var forecasts []string
    for i := range min(5, len(periods)) {
        forecasts = append(forecasts, formatPeriod(periods[i]))
    }

    result := strings.Join(forecasts, "\n---\n")

    return &mcp.CallToolResult{
        Content: []mcp.Content{
            &mcp.TextContent{Text: result},
        },
    }, nil, nil
}

func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
    *mcp.CallToolResult, any, error,
) {
    // Build alerts URL
    stateCode := strings.ToUpper(input.State)
    alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)

    alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
    if err != nil {
        return &mcp.CallToolResult{
            Content: []mcp.Content{
                &mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
            },
        }, nil, nil
    }

    // Check if there are any alerts
    if len(alertsData.Features) == 0 {
        return &mcp.CallToolResult{
            Content: []mcp.Content{
                &mcp.TextContent{Text: "No active alerts for this state."},
            },
        }, nil, nil
    }

    // Format alerts
    var alerts []string
    for _, feature := range alertsData.Features {
        alerts = append(alerts, formatAlert(feature))
    }

    result := strings.Join(alerts, "\n---\n")

    return &mcp.CallToolResult{
        Content: []mcp.Content{
            &mcp.TextContent{Text: result},
        },
    }, nil, nil
}

서버 실행

마지막으로 서버를 실행하는 main 함수를 구현합니다.

func main() {
    // Create MCP server
    server := mcp.NewServer(&mcp.Implementation{
        Name:    "weather",
        Version: "1.0.0",
    }, nil)

    // Add get_forecast tool
    mcp.AddTool(server, &mcp.Tool{
        Name:        "get_forecast",
        Description: "Get weather forecast for a location",
    }, getForecast)

    // Add get_alerts tool
    mcp.AddTool(server, &mcp.Tool{
        Name:        "get_alerts",
        Description: "Get weather alerts for a US state",
    }, getAlerts)

    // Run server on stdio transport
    if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
        log.Fatal(err)
    }
}

다음 명령으로 서버를 빌드하세요.

go build -o weather .

컴파일된 바이너리는 ./weather에 있을 거예요. 이제 기존 MCP 호스트인 Claude for Desktop에서 서버를 테스트해 볼게요.

Claude for Desktop으로 서버 테스트하기

먼저 Claude for Desktop이 설치되어 있어야 해요. 여기서 최신 버전을 설치할 수 있고, 이미 설치되어 있다면 최신 버전으로 업데이트했는지 확인해 주세요.

사용할 MCP 서버마다 Claude for Desktop을 설정해야 해요. Claude for Desktop 앱 설정 파일인 ~/Library/Application Support/Claude/claude_desktop_config.json을 텍스트 편집기로 열면 됩니다. 파일이 없으면 직접 만들어 주세요.

VS Code가 설치되어 있다면 이렇게 열 수 있어요.

Linux

code ~/.config/Claude/claude_desktop_config.json

macOS

code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows

code $env:AppData\Claude\claude_desktop_config.json

그다음 mcpServers 키에 서버를 추가하면 돼요. MCP UI 요소는 서버가 최소 하나 이상 제대로 설정되어 있을 때만 Claude for Desktop에 나타나요. 이번에는 날씨 서버 하나를 이렇게 추가해 볼게요.

macOS/Linux

{
  "mcpServers": {
    "weather": {
      "command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
    }
  }
}

Windows

{
  "mcpServers": {
    "weather": {
      "command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
    }
  }
}

컴파일된 바이너리의 절대 경로를 전달해 주세요. 프로젝트 디렉터리에서 macOS/Linux에서는 pwd, Windows 명령 프롬프트에서는 cd로 확인할 수 있어요. Windows에서는 JSON 경로에 이중 백슬래시(\\)나 슬래시(/)를 사용하고 .exe 확장자를 붙여야 합니다.

이 설정이 Claude for Desktop에 알려주는 내용은 두 가지예요.

  1. "weather"라는 MCP 서버가 있다.
  2. 지정된 경로의 컴파일된 바이너리로 실행한다.

파일을 저장하고 Claude for Desktop을 다시 시작해 주세요.

명령으로 테스트하기

Claude for Desktop이 우리 weather 서버가 노출한 두 도구를 인식하고 있는지 확인해 볼게요. "파일, 커넥터 등 추가하기 /" 아이콘을 찾으면 됩니다. 플러스 아이콘을 클릭한 뒤 "Connectors" 메뉴에 마우스를 올리면 weather 서버가 목록에 보일 거예요.

서버가 Claude for Desktop에 인식되지 않으면 Troubleshooting 섹션에서 디버깅 팁을 확인하세요. 서버가 "Connectors" 메뉴에 나타났다면, Claude for Desktop에서 다음 명령으로 서버를 테스트해 볼 수 있어요.

  • Sacramento의 날씨는 어떤가요?
  • 텍사스에 현재 날씨 경보가 있나요?

이 서버는 미국 국립기상청을 사용하므로 쿼리는 미국 지역에서만 동작해요.

내부에서는 무슨 일이 일어날까요

질문을 하면 이런 일이 일어나요.

  1. 클라이언트가 사용자의 질문을 Claude에 보냅니다.
  2. Claude가 사용 가능한 도구를 분석하고 어떤 도구(들)를 사용할지 결정합니다.
  3. 클라이언트가 MCP 서버를 통해 선택한 도구(들)를 실행합니다.
  4. 결과가 Claude로 다시 보내집니다.
  5. Claude가 자연어 응답을 만듭니다.
  6. 그 응답이 여러분에게 표시됩니다!

문제 해결

Claude for Desktop 통합 문제

Claude for Desktop에서 로그 가져오기 MCP와 관련된 Claude.app 로깅은 ~/Library/Logs/Claude(macOS)나 ~/.config/Claude/logs/(Linux)의 로그 파일에 기록됩니다.

  • mcp.log에는 MCP 연결과 연결 실패에 대한 일반 로깅이 들어 있어요.
  • mcp-server-SERVERNAME.log라는 이름의 파일에는 해당 서버의 stderr 출력이 들어 있습니다. Stdio 서버는 모든 로깅에 stderr를 쓸 수 있으므로, 이 파일은 오류만 담지는 않습니다.

다음 명령으로 최근 로그 목록을 확인하고 새 로그를 따라가 볼 수 있어요.

macOS

# Check Claude's logs for errors
tail -n 20 -f ~/Library/Logs/Claude/mcp*.log

Linux

# Check Claude's logs for errors
tail -n 20 -f ~/.config/Claude/logs/mcp*.log

서버가 Claude에 나타나지 않음

  1. claude_desktop_config.json 파일 구문을 확인하세요.
  2. 프로젝트 경로가 상대 경로가 아닌 절대 경로인지 확인하세요.
  3. Claude for Desktop을 완전히 다시 시작하세요.

Claude for Desktop을 제대로 다시 시작하려면 애플리케이션을 완전히 종료해야 해요.

  • Windows: 시스템 트레이의 Claude 아이콘(숨겨진 아이콘 메뉴에 있을 수 있음)을 오른쪽 클릭하고 "Quit" 또는 "Exit"를 선택하세요.
  • macOS: Cmd+Q를 사용하거나 메뉴 막대에서 "Quit Claude"를 선택하세요.
  • Linux: 시스템 트레이의 Claude 아이콘을 오른쪽 클릭하고 "Quit"를 선택하거나, 터미널에서 pkill -f claude-desktop을 실행하세요.

창을 닫는 것만으로는 애플리케이션이 완전히 종료되지 않아서, MCP 서버 설정 변경이 적용되지 않습니다.

도구 호출이 조용히 실패함 Claude가 도구를 사용하려는데 실패한다면:

  1. Claude의 로그에서 오류를 확인하세요.
  2. 서버가 오류 없이 빌드되고 실행되는지 확인하세요.
  3. Claude for Desktop을 다시 시작해 보세요.

아무것도 해결되지 않아요. 어떻게 해야 하나요? 더 나은 디버깅 도구와 자세한 안내는 디버깅 가이드를 참고해 주세요.

날씨 API 문제

오류: 그리드 포인트 데이터를 가져오지 못했습니다 이는 보통 다음 중 하나를 의미해요.

  1. 좌표가 미국 밖에 있다.
  2. NWS API에 문제가 있다.
  3. 요청이 rate limit에 걸렸다.

해결 방법:

  • 미국 좌표를 사용하고 있는지 확인하세요.
  • 요청 사이에 약간의 지연을 추가하세요.
  • NWS API 상태 페이지를 확인하세요.

오류: [STATE]에 대한 활성 경보 없음 이는 오류가 아니라 그 주(state)에 현재 날씨 경보가 없다는 뜻이에요. 다른 주를 시도하거나, 심각한 날씨 중에 확인해 보세요.

더 고급 문제 해결은 MCP 디버깅 가이드를 확인하세요.

다음 단계