MCP 서버 구축하기
MCP 서버 구축하기 (Build an MCP server)
Claude for Desktop과 다른 클라이언트에서 사용할 나만의 서버를 만드는 방법을 배워 보세요. 이 튜토리얼에서는 간단한 MCP 날씨 서버를 만들고 이를 호스트(Claude for Desktop)에 연결해 봅니다.
출처: 문서
본문
이 튜토리얼에서는 간단한 MCP 날씨 서버를 만들고 이를 호스트인 Claude for Desktop에 연결해 봅니다.
무엇을 만들까요?
get_alerts와 get_forecast 두 가지 도구를 노출하는 서버를 만들게요. 그리고 그 서버를 MCP 호스트(여기서는 Claude for Desktop)에 연결합니다.
서버는 어떤 클라이언트와도 연결할 수 있어요. 여기서는 단순함을 위해 Claude for Desktop을 선택했지만, 나만의 클라이언트 구축 가이드도 따로 있어요.
핵심 MCP 개념
MCP 서버는 세 가지 주요 유형의 기능을 제공할 수 있어요.
- 리소스: 클라이언트가 읽을 수 있는 파일 같은 데이터(API 응답이나 파일 내용처럼)
- 도구: 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__)
# ❌ Bad (STDIO)
print("Processing request")
# ✅ Good (STDIO)
logger.info("Processing request") # writes to stderr
시스템 요구 사항
- Python 3.10 이상 설치.
- Python MCP SDK 2.0.0 이상 사용.
환경 설정
먼저 uv를 설치하고 Python 프로젝트와 환경을 설정할게요.
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]"
# 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]
# 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이 설치되어 있는지 확인하세요. 최신 버전은 여기에서 설치할 수 있어요. 이미 Claude for Desktop이 있다면 최신 버전으로 업데이트했는지 확인하세요.
사용하려는 MCP 서버에 맞게 Claude for Desktop을 구성해야 해요. 이를 위해 텍스트 편집기로 Claude for Desktop 앱 구성(~/Library/Application Support/Claude/claude_desktop_config.json)을 열어야 합니다. 파일이 없으면 만들어야 해요.
예를 들어 VS Code가 설치되어 있다면:
code ~/.config/Claude/claude_desktop_config.json
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
code $env:AppData\Claude\claude_desktop_config.json
그런 다음 mcpServers 키에 서버를 추가해요. MCP UI 요소는 최소한 하나의 서버가 제대로 구성된 경우에만 Claude for Desktop에 나타납니다.
이 경우 단일 날씨 서버를 다음과 같이 추가할게요.
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
"run",
"weather.py"
]
}
}
}
{
"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에 알려 줍니다:
- "weather"라는 MCP 서버가 있다는 것
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나 파일에 쓰는 로깅 라이브러리를 사용하세요.
빠른 예시
// ❌ Bad (STDIO)
console.log("Server started");
// ✅ Good (STDIO)
console.error("Server started"); // stderr is safe
시스템 요구 사항
TypeScript는 최신 버전의 Node가 설치되어 있는지 확인하세요.
환경 설정
먼저 아직 설치하지 않았다면 Node.js와 npm을 설치할게요. nodejs.org에서 다운로드할 수 있어요. Node.js 설치를 확인하세요.
node --version
npm --version
이 튜토리얼에는 Node.js 버전 20 이상이 필요해요.
이제 프로젝트를 만들고 설정할게요.
# 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
# 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.json에 type: "module"과 빌드 스크립트를 추가하도록 업데이트하세요.
{
"type": "module",
"bin": {
"weather": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
},
"files": ["build"]
}
프로젝트 루트에 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",
});
헬퍼 함수
다음으로 National Weather Service 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,
},
],
};
},
);
서버 실행하기
마지막으로 서버를 실행하는 메인 함수를 구현할게요.
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이 설치되어 있는지 확인하세요. 최신 버전은 여기에서 설치할 수 있어요. 이미 Claude for Desktop이 있다면 최신 버전으로 업데이트했는지 확인하세요.
사용하려는 MCP 서버에 맞게 Claude for Desktop을 구성해야 해요. 이를 위해 텍스트 편집기로 Claude for Desktop 앱 구성(~/Library/Application Support/Claude/claude_desktop_config.json)을 열어야 합니다. 파일이 없으면 만들어야 해요.
예를 들어 VS Code가 설치되어 있다면:
code ~/.config/Claude/claude_desktop_config.json
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
code $env:AppData\Claude\claude_desktop_config.json
그런 다음 mcpServers 키에 서버를 추가해요. MCP UI 요소는 최소한 하나의 서버가 제대로 구성된 경우에만 Claude for Desktop에 나타납니다.
이 경우 단일 날씨 서버를 다음과 같이 추가할게요.
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"]
}
}
}
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\index.js"]
}
}
}
이렇게 해서 Claude for Desktop에 알려 줍니다:
- "weather"라는 MCP 서버가 있다는 것
node /ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js로 실행한다는 것
파일을 저장하고 Claude for Desktop을 다시 시작하세요.
Java
이는 Spring AI MCP 자동 구성과 부트 스타터를 기반으로 한 빠른 시작 데모예요. 동기·비동기 MCP 서버를 수동으로 만드는 방법은 Java SDK Server 문서를 참고하세요.
날씨 서버 구축을 시작해 볼게요! 여기에서 만들 내용의 완전한 코드를 확인할 수 있어요.
더 자세한 내용은 MCP Server Boot Starter 참조 문서를 확인하세요. 수동 MCP 서버 구현은 MCP Server Java SDK 문서를 참고하세요.
MCP 서버에서의 로깅
MCP 서버를 구현할 때 로깅 처리에 주의하세요.
STDIO 기반 서버: 절대 System.out.println()이나 System.out.print()를 사용하지 마세요. 표준 출력(stdout)에 쓰기 때문이에요. stdout에 쓰면 JSON-RPC 메시지를 손상시켜 서버를 망가뜨립니다.
HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.
모범 사례
- stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.
- 구성된 로깅 라이브러리가 stdout에 쓰지 않도록 확인하세요.
시스템 요구 사항
- Java 17 이상 설치.
- Spring Boot 3.3.x 이상
환경 설정
Spring Initializer로 프로젝트를 부트스트랩하세요.
다음 의존성을 추가해야 해요.
<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>
dependencies {
implementation platform("org.springframework.ai:spring-ai-starter-mcp-server")
implementation platform("org.springframework:spring-web")
}
그런 다음 애플리케이션 속성을 설정해 애플리케이션을 구성하세요.
spring.main.bannerMode=off
logging.pattern.console=
logging:
pattern:
console:
spring:
main:
banner-mode: off
서버 구성 속성 문서에 사용 가능한 모든 속성이 나와 있어요.
이제 서버 구축에 들어가 볼게요.
서버 구축하기
날씨 서비스
National Weather Service API에서 데이터를 조회하는 REST 클라이언트를 사용하는 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이 설치되어 있는지 확인하세요. 최신 버전은 여기에서 설치할 수 있어요. 이미 Claude for Desktop이 있다면 최신 버전으로 업데이트했는지 확인하세요.
사용하려는 MCP 서버에 맞게 Claude for Desktop을 구성해야 해요. 이를 위해 텍스트 편집기로 Claude for Desktop 앱 구성(~/Library/Application Support/Claude/claude_desktop_config.json)을 열어야 합니다. 파일이 없으면 만들어야 해요.
예를 들어 VS Code가 설치되어 있다면:
code ~/.config/Claude/claude_desktop_config.json
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
code $env:AppData\Claude\claude_desktop_config.json
그런 다음 mcpServers 키에 서버를 추가해요. MCP UI 요소는 최소한 하나의 서버가 제대로 구성된 경우에만 Claude for Desktop에 나타납니다.
이 경우 단일 날씨 서버를 다음과 같이 추가할게요.
{
"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"
]
}
}
}
{
"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에 알려 줍니다:
- "my-weather-server"라는 MCP 서버가 있다는 것
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 클라이언트 Boot 스타터 사용
spring-ai-starter-mcp-client 의존성으로 새 boot 스타터 애플리케이션을 만드세요.
<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 클라이언트 Boot 스타터 참조 문서를 확인하세요.
더 많은 Java MCP 서버 예시
starter-webflux-server는 WebFlux 스타터로 HTTP 기반 MCP 서버를 만드는 방법을 보여 줍니다. spring.ai.mcp.server.protocol=STREAMABLE 속성을 설정하면 Streamable HTTP로 서빙돼요. 이 예시는 Spring Boot의 자동 구성 기능을 사용해 MCP 도구, 리소스, 프롬프트를 정의하고 등록하는 방법을 보여 줍니다.
Kotlin
날씨 서버 구축을 시작해 볼게요! 여기에서 만들 내용의 완전한 코드를 확인할 수 있어요.
사전 지식
이 빠른 시작은 다음에 익숙하다고 가정합니다.
- Kotlin
- Claude 같은 LLM
MCP 서버에서의 로깅
MCP 서버를 구현할 때 로깅 처리에 주의하세요.
STDIO 기반 서버: 절대 println()을 사용하지 마세요. 기본적으로 표준 출력(stdout)에 쓰기 때문이에요. stdout에 쓰면 JSON-RPC 메시지를 손상시켜 서버를 망가뜨립니다.
HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.
모범 사례
- stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.
시스템 요구 사항
- JDK 11 이상 설치.
환경 설정
먼저 아직 설치하지 않았다면 java와 gradle을 설치할게요. java는 공식 Oracle JDK 웹사이트에서 다운로드할 수 있어요. java 설치를 확인하세요.
java --version
이제 프로젝트를 만들고 설정할게요.
# Create a new directory for our project
mkdir weather
cd weather
# Initialize a new kotlin project
gradle init
# 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 내용을 다음으로 바꾸세요.
// 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 헬퍼 함수
다음으로 National Weather Service 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) })
}
서버 실행하기
마지막으로 서버를 실행하는 메인 함수를 구현할게요.
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이 설치되어 있는지 확인하세요. 최신 버전은 여기에서 설치할 수 있어요. 이미 Claude for Desktop이 있다면 최신 버전으로 업데이트했는지 확인하세요.
사용하려는 MCP 서버에 맞게 Claude for Desktop을 구성해야 해요. 이를 위해 텍스트 편집기로 Claude for Desktop 앱 구성(~/Library/Application Support/Claude/claude_desktop_config.json)을 열어야 합니다. 파일이 없으면 만들어야 해요.
예를 들어 VS Code가 설치되어 있다면:
code ~/.config/Claude/claude_desktop_config.json
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
code $env:AppData\Claude\claude_desktop_config.json
그런 다음 mcpServers 키에 서버를 추가해요. MCP UI 요소는 최소한 하나의 서버가 제대로 구성된 경우에만 Claude for Desktop에 나타납니다.
이 경우 단일 날씨 서버를 다음과 같이 추가할게요.
{
"mcpServers": {
"weather": {
"command": "java",
"args": [
"-jar",
"/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/libs/weather-0.1.0-all.jar"
]
}
}
}
{
"mcpServers": {
"weather": {
"command": "java",
"args": [
"-jar",
"C:\\PATH\\TO\\PARENT\\FOLDER\\weather\\build\\libs\\weather-0.1.0-all.jar"
]
}
}
}
이렇게 해서 Claude for Desktop에 알려 줍니다:
- "weather"라는 MCP 서버가 있다는 것
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을 설치할게요. dotnet은 공식 Microsoft .NET 웹사이트에서 다운로드할 수 있어요. dotnet 설치를 확인하세요.
dotnet --version
이제 프로젝트를 만들고 설정할게요.
# Create a new directory for our project
mkdir weather
cd weather
# Initialize a new C# project
dotnet new console
# Create a new directory for our project
mkdir weather
cd weather
# Initialize a new C# project
dotnet new console
dotnet new console를 실행하면 새 C# 프로젝트가 생겨요. Visual Studio나 Rider 같은 선호하는 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 전송을 사용하는 서버에만 필요한 사항이에요.
이 코드는 Model Context Protocol SDK를 사용해 표준 I/O 전송으로 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());
}
}
다음으로 National Weather Service 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이 설치되어 있는지 확인하세요. 최신 버전은 여기에서 설치할 수 있어요. 이미 Claude for Desktop이 있다면 최신 버전으로 업데이트했는지 확인하세요. 사용하려는 MCP 서버에 맞게 Claude for Desktop을 구성해야 해요. 이를 위해 텍스트 편집기로 Claude for Desktop 앱 구성(~/Library/Application Support/Claude/claude_desktop_config.json)을 열어야 합니다. 파일이 없으면 만들어야 해요. 예를 들어 VS Code가 설치되어 있다면:
code ~/.config/Claude/claude_desktop_config.json
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
code $env:AppData\Claude\claude_desktop_config.json
그런 다음 mcpServers 키에 서버를 추가해요. MCP UI 요소는 최소한 하나의 서버가 제대로 구성된 경우에만 Claude for Desktop에 나타납니다. 이 경우 단일 날씨 서버를 다음과 같이 추가할게요.
{
"mcpServers": {
"weather": {
"command": "dotnet",
"args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
}
}
}
{
"mcpServers": {
"weather": {
"command": "dotnet",
"args": [
"run",
"--project",
"C:\\ABSOLUTE\\PATH\\TO\\PROJECT",
"--no-build"
]
}
}
}
이렇게 해서 Claude for Desktop에 알려 줍니다:
- "weather"라는 MCP 서버가 있다는 것
dotnet run /ABSOLUTE/PATH/TO/PROJECT로 실행한다는 것 파일을 저장하고 Claude for Desktop을 다시 시작하세요.
Ruby
날씨 서버 구축을 시작해 볼게요! 여기에서 만들 내용의 완전한 코드를 확인할 수 있어요.
사전 지식
이 빠른 시작은 다음에 익숙하다고 가정합니다.
- Ruby
- Claude 같은 LLM
MCP 서버에서의 로깅
MCP 서버를 구현할 때 로깅 처리에 주의하세요.
STDIO 기반 서버: 절대 puts나 print를 사용하지 마세요. 기본적으로 표준 출력(stdout)에 쓰기 때문이에요. stdout에 쓰면 JSON-RPC 메시지를 손상시켜 서버를 망가뜨립니다.
HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.
모범 사례
- stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.
빠른 예시
# ❌ Bad (STDIO)
puts "Processing request"
# ✅ Good (STDIO)
require "logger"
logger = Logger.new($stderr)
logger.info("Processing request")
시스템 요구 사항
- Ruby 2.7 이상 설치.
환경 설정
먼저 Ruby가 설치되어 있는지 확인할게요. 다음을 실행해 확인할 수 있어요.
ruby --version
이제 프로젝트를 만들고 설정할게요.
# 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
# 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 젬은 Ruby용 Model Context Protocol SDK를 제공하며, 서버 구현과 stdio 전송을 위한 클래스를 담고 있어요.
헬퍼 메서드
다음으로 National Weather Service 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이 설치되어 있는지 확인하세요. 최신 버전은 여기에서 설치할 수 있어요. 이미 Claude for Desktop이 있다면 최신 버전으로 업데이트했는지 확인하세요.
사용하려는 MCP 서버에 맞게 Claude for Desktop을 구성해야 해요. 이를 위해 텍스트 편집기로 Claude for Desktop 앱 구성(~/Library/Application Support/Claude/claude_desktop_config.json)을 열어야 합니다. 파일이 없으면 만들어야 해요.
예를 들어 VS Code가 설치되어 있다면:
code ~/.config/Claude/claude_desktop_config.json
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
code $env:AppData\Claude\claude_desktop_config.json
그런 다음 mcpServers 키에 서버를 추가해요. MCP UI 요소는 최소한 하나의 서버가 제대로 구성된 경우에만 Claude for Desktop에 나타납니다.
이 경우 단일 날씨 서버를 다음과 같이 추가할게요.
{
"mcpServers": {
"weather": {
"command": "bundle",
"args": ["exec", "ruby", "weather.rb"],
"cwd": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather"
}
}
}
{
"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에 알려 줍니다:
- "weather"라는 MCP 서버가 있다는 것
- 지정된 디렉터리에서
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 출력을 피하도록 구성하세요.
빠른 예시
// ❌ Bad (STDIO)
println!("Processing request");
// ✅ Good (STDIO)
eprintln!("Processing request"); // writes to stderr
시스템 요구 사항
- Rust 1.70 이상 설치.
- Cargo(Rust 설치에 포함).
환경 설정
먼저 아직 설치하지 않았다면 Rust를 설치할게요. Rust는 rust-lang.org에서 설치할 수 있어요.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Download and run rustup-init.exe from https://rustup.rs/
Rust 설치를 확인하세요.
rustc --version
cargo --version
이제 프로젝트를 만들고 설정할게요.
# Create a new Rust project
cargo new weather
cd weather
# Create a new Rust project
cargo new weather
cd weather
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 전송을 위한 기능을 담고 있어요.
데이터 구조
다음으로 National Weather Service 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 전송으로 서버를 실행하는 메인 함수를 구현할게요.
#[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이 설치되어 있는지 확인하세요. 최신 버전은 여기에서 설치할 수 있어요. 이미 Claude for Desktop이 있다면 최신 버전으로 업데이트했는지 확인하세요.
사용하려는 MCP 서버에 맞게 Claude for Desktop을 구성해야 해요. 이를 위해 텍스트 편집기로 Claude for Desktop 앱 구성(~/Library/Application Support/Claude/claude_desktop_config.json)을 열어야 합니다. 파일이 없으면 만들어야 해요.
예를 들어 VS Code가 설치되어 있다면:
code ~/.config/Claude/claude_desktop_config.json
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
code $env:AppData\Claude\claude_desktop_config.json
그런 다음 mcpServers 키에 서버를 추가해요. MCP UI 요소는 최소한 하나의 서버가 제대로 구성된 경우에만 Claude for Desktop에 나타납니다.
이 경우 단일 날씨 서버를 다음과 같이 추가할게요.
{
"mcpServers": {
"weather": {
"command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/target/release/weather"
}
}
}
{
"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에 알려 줍니다:
- "weather"라는 MCP 서버가 있다는 것
- 지정된 경로의 컴파일된 바이너리로 실행한다는 것
파일을 저장하고 Claude for Desktop을 다시 시작하세요.
Go
날씨 서버 구축을 시작해 볼게요! 여기에서 만들 내용의 완전한 코드를 확인할 수 있어요.
사전 지식
이 빠른 시작은 다음에 익숙하다고 가정합니다.
- Go
- Claude 같은 LLM
MCP 서버에서의 로깅
MCP 서버를 구현할 때 로깅 처리에 주의하세요.
STDIO 기반 서버: 절대 fmt.Println()이나 fmt.Printf()를 사용하지 마세요. 표준 출력(stdout)에 쓰기 때문이에요. stdout에 쓰면 JSON-RPC 메시지를 손상시켜 서버를 망가뜨립니다.
HTTP 기반 서버: 표준 출력 로깅은 HTTP 응답을 방해하지 않으므로 괜찮아요.
모범 사례
log.Println()(기본값이 stderr) 또는 stderr나 파일에 쓰는 로깅 라이브러리를 사용하세요.fmt.Fprintf(os.Stderr, ...)로 stderr에 명시적으로 쓰세요.
빠른 예시
// ❌ Bad (STDIO)
fmt.Println("Processing request")
// ✅ Good (STDIO)
log.Println("Processing request") // defaults to stderr
// ✅ Good (STDIO)
fmt.Fprintln(os.Stderr, "Processing request")
시스템 요구 사항
- Go 1.24 이상 설치.
환경 설정
먼저 아직 설치하지 않았다면 Go를 설치할게요. Go는 go.dev에서 다운로드하고 설치할 수 있어요.
Go 설치를 확인하세요.
go version
이제 프로젝트를 만들고 설정할게요.
# 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
# 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)"`
}
헬퍼 함수
다음으로 National Weather Service 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
}
서버 실행하기
마지막으로 서버를 실행하는 메인 함수를 구현할게요.
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이 설치되어 있는지 확인하세요. 최신 버전은 여기에서 설치할 수 있어요. 이미 Claude for Desktop이 있다면 최신 버전으로 업데이트했는지 확인하세요.
사용하려는 MCP 서버에 맞게 Claude for Desktop을 구성해야 해요. 이를 위해 텍스트 편집기로 Claude for Desktop 앱 구성(~/Library/Application Support/Claude/claude_desktop_config.json)을 열어야 합니다. 파일이 없으면 만들어야 해요.
예를 들어 VS Code가 설치되어 있다면:
code ~/.config/Claude/claude_desktop_config.json
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
code $env:AppData\Claude\claude_desktop_config.json
그런 다음 mcpServers 키에 서버를 추가해요. MCP UI 요소는 최소한 하나의 서버가 제대로 구성된 경우에만 Claude for Desktop에 나타납니다.
이 경우 단일 날씨 서버를 다음과 같이 추가할게요.
{
"mcpServers": {
"weather": {
"command": "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/weather"
}
}
}
{
"mcpServers": {
"weather": {
"command": "C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather\\weather.exe"
}
}
}
컴파일된 바이너리의 절대 경로를 전달해야 해요. 이 값은 프로젝트 디렉터리에서 macOS/Linux의
pwd또는 Windows 명령 프롬프트의cd로 얻을 수 있어요. Windows에서는 JSON 경로에 이중 백슬래시(\\)나 슬래시(/)를 사용하고.exe확장자를 추가하는 것을 기억하세요.
이렇게 해서 Claude for Desktop에 알려 줍니다:
- "weather"라는 MCP 서버가 있다는 것
- 지정된 경로의 컴파일된 바이너리로 실행한다는 것
파일을 저장하고 Claude for Desktop을 다시 시작하세요.
명령으로 테스트하기
Claude for Desktop이 우리 weather 서버에서 노출한 두 도구를 인식하는지 확인해 볼게요. "Add files, connectors, and more /" 아이콘을 찾아 확인할 수 있어요.
더하기 아이콘을 클릭한 후 "Connectors" 메뉴 위에 마우스를 올려 보세요. weather 서버가 나열되어 있어야 해요.
서버가 Claude for Desktop에서 인식되지 않으면 문제 해결 섹션으로 진행해 디버깅 팁을 확인하세요.
서버가 "Connectors" 메뉴에 나타났다면, Claude for Desktop에서 다음 명령을 실행해 서버를 테스트할 수 있어요.
- 새크라멘토 날씨는 어떤가요?
- 텍사스의 활성 날씨 알림은 무엇인가요?
이것은 미국 국립 기상청(National Weather Service)이므로, 쿼리는 미국 지역에서만 동작해요.
내부에서 일어나는 일
질문을 하면:
- 클라이언트가 질문을 Claude로 보내요
- Claude가 사용 가능한 도구를 분석하고 어떤 것을 사용할지 결정해요
- 클라이언트가 선택한 도구를 MCP 서버를 통해 실행해요
- 결과가 Claude로 다시 전송돼요
- Claude가 자연어 응답을 구성해요
- 응답이 사용자에게 표시돼요!
문제 해결 (Troubleshooting)
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를 사용할 수 있으므로, 이 파일은 오류에 국한되지 않아요.
다음 명령으로 최근 로그를 나열하고 새 로그를 따라갈 수 있어요.
# Check Claude's logs for errors
tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
# Check Claude's logs for errors
tail -n 20 -f ~/.config/Claude/logs/mcp*.log
서버가 Claude에 나타나지 않는 경우
claude_desktop_config.json파일 문법을 확인하세요- 프로젝트 경로가 상대 경로가 아닌 절대 경로인지 확인하세요
- Claude for Desktop을 완전히 다시 시작하세요
Claude for Desktop을 제대로 다시 시작하려면 애플리케이션을 완전히 종료해야 해요:
- Windows: 시스템 트레이의 Claude 아이콘(숨겨진 아이콘 메뉴에 있을 수 있음)을 마우스 오른쪽 클릭하고 "Quit" 또는 "Exit"을 선택하세요.
- macOS: Cmd+Q를 사용하거나 메뉴 막대에서 "Quit Claude"를 선택하세요.
- Linux: 시스템 트레이의 Claude 아이콘을 마우스 오른쪽 클릭하고 "Quit"을 선택하거나, 터미널에서
pkill -f claude-desktop을 실행하세요.창을 그냥 닫는 것만으로는 애플리케이션이 완전히 종료되지 않아서, MCP 서버 구성 변경이 적용되지 않아요.
도구 호출이 조용히 실패하는 경우
Claude가 도구를 사용하려 하지만 실패한다면:
- 오류를 위해 Claude의 로그를 확인하세요
- 서버가 오류 없이 빌드되고 실행되는지 확인하세요
- Claude for Desktop 재시작을 시도하세요
이것도 안 되면 어떻게 하나요?
더 나은 디버깅 도구와 상세한 안내는 디버깅 가이드를 참고하세요.
Weather API 문제
오류: 그리드 포인트 데이터를 검색하는 데 실패했습니다
이것은 보통 다음 중 하나를 의미해요.
- 좌표가 미국 밖에 있음
- NWS API에 문제가 있음
- 속도 제한에 걸림
수정:
- 미국 좌표를 사용하고 있는지 확인하세요
- 요청 사이에 짧은 지연을 추가하세요
- NWS API 상태 페이지를 확인하세요
오류: [STATE]에 대한 활성 알림 없음
이것은 오류가 아니에요. 그 주에 대한 현재 날씨 알림이 없다는 뜻일 뿐입니다. 다른 주를 시도하거나 악천후 때 확인하세요.
더 고급 문제 해결은 MCP 디버깅 가이드를 확인하세요.
다음 단계
- 클라이언트 구축 — 서버에 연결할 수 있는 나만의 MCP 클라이언트 구축법 배우기
- 예시 서버 — 공식 MCP 서버와 구현 갤러리 살펴보기
- 디버깅 가이드 — MCP 서버와 통합을 효과적으로 디버깅하는 법 배우기
- 에이전트 스킬로 구축 — 에이전트 스킬로 AI 코딩 어시스턴트가 서버 설계를 안내하게 하기