LLM 성능 극대화: MCP를 통해 효과적인 AI 애플리케이션 구축하기

2024년 11월 Anthropic이 MCP라는 새로운 프로토콜을 발표했습니다. 자세한 것은 이곳을 참조하세요.
이번 글에서는 MCP에 대해 자세히 알아보겠습니다.
MCP 소개
MCP는 Model Context Protocol의 약자입니다.
여기서 Model은 GPT, Claude, Gemini와 같은 AI model을 의미합니다. 그리고 Context는 Model에게 제공하는 추가적인 외부 정보를 나타내는 용어입니다. Protocol은 말 그대로 규약이라는 뜻입니다.
종합해보면 아래와 같이 한 문장으로 요약할 수 있습니다.
AI Model에 외부 정보를 제공하기 위한 규약
자, 이제 좀 더 자세히 알아봅시다.
Model에 Context를 제공하는 이유
일단, Model에 Context를 제공하는 이유에 대해 알아보겠습니다. 이것을 먼저 알아야 MCP를 왜 만들게 되었는지 비로소 이해할 수 있기 때문입니다.
GPT와 같은 LLM(Large Language Model)은 기술적 특성으로 인해 Knowledge cut-off date 라는 것을 갖고 있습니다. 사전 학습 단계에서 방대한 양의 텍스트를 학습하지만 그 사전 학습을 수행하는 시점까지의 정보만 알고 있는 것이죠.

위 이미지는 주요 LLM 들의 Knowledge cut-off 날짜만 모아놓은 Githug repo에서 OpenAI 모델 부분만 캡쳐한 것입니다. 자세히 보면 각 Model 별로 Cut-off Date
항목이 있고 이것이 특정일로 지정되어 있다는 것을 알 수 있습니다. OpenAI 모델 뿐만 아니라 모든 LLM은 전부 이 Cut-off date를 갖고 있습니다.
즉, LLM이 자체적으로 알고 있는 정보는 딱 Cut-off 날짜까지이고 그 이후의 정보는 대답할 수 없는 것입니다. 이 경우에는 오히려 그 날짜 이후 정보에 대해서는 LLM이 답변해서는 안 됩니다. 거짓 정보인 셈이니까요. LLM의 Cut-off date를 테스트하는 방법은 아주 쉽습니다. 아무 LLM 서비스에 접속해서 오늘 서울 날씨는 어때?(How is the weather in Seoul today?)
라고 물어보는 것입니다.

Claude를 사용해보니, 오늘 날짜의 서울 날씨는 알 수 없다고 합니다.
이 글을 작성하는 2025년 3월 19일 현재의 날씨 정보를 갖고 있는 LLM이 있다면
아마 미래에 발표될 모델일 것입니다. 지금 열심히 사전 학습 중이겠죠.
그러면 날짜를 바꿔볼까요?
방금 사용한 Claude 3.7 Sonnet의 Knowledge cut-off 날짜가 2024년 10월이라고 하니 그 이전 날짜의 날씨를 물어보겠습니다.

2024년 3월의 서울 날씨를 물어보니 해당 날짜의 날씨 정보가 훈련 데이터에 없다고 하는군요.
이 2개의 질의를 통해 우리는 LLM이 태생적으로 갖고 있는 한계점 2가지를 파악할 수 있습니다.
- Knowledge Cut-off date
- Cut-off 날짜 이후에 발생한 사건이나 정보에 대해 LLM은 알 수 없습니다
- Pre-training 또는 Post-training 과정에 포함되지 못한 정보
- 비록 Cut-off 날짜 이전에 생겨난 정보라고 해도 학습 과정에 포함되지 못한 경우 역시 LLM은 알 수 없습니다
날씨 정보에 대한 아주 간단한 질의조차도 LLM은 제대로 처리할 수 없습니다. 그래서 실시간으로 현재 또는 사용자가 원하는 시점의 적확한 정보를 조회해서 LLM에게 주입시켜야 합니다.
이것을 In-context learning 이라고 합니다. LLM이 Task를 올바르게 처리할 수 있도록 필요한 정보를 모두 제공하는 것입니다. ChatGPT가 나오고 얼마 지나지 않았을 무렵을 기억하시나요? 그 당시 사람들은 채팅 인터페이스에 사람이 직접 검색한 정보를 복사/붙여넣기 했었습니다. 그리고 이 정보를 토대로 추가 질의를 했었죠.
MCP는 LLM에게 Context를 제공하는 방식에 대한 프로토콜을 의미합니다. 사람이 직접 필요한 정보를 Copy&Paste 해서 채팅창에 붙여넣기 하는 것이 아니라 LLM(또는 Agent, Client)이 동적으로 Context를 조회/수정할 수 있게 하는 것입니다.
지금까지의 설명만 듣고 MCP에 대해 전부 이해하기는 힘듭니다. 이 글에서는 앞서 잠깐 테스트했던 '날씨'에 대한 use case를 기반으로 MCP가 구체적으로 어떻게 동작하는지 확인하면서 좀 더 깊은 이해를 할 수 있도록 도움을 드리겠습니다.
MCP가 등장하게 된 배경
그러면 MCP가 등장하게 된 배경에 대해 좀 더 알아보겠습니다. 이 부분까지 이해하고 MCP의 구체적인 구성 요소와 실제 코드를 보면 더욱 빠르게 적응할 수 있기 때문입니다.
MCP를 사용하지 않고 LLM 기반의 서비스를 개발한다고 했을 때의 몇가지 어려움들이 있습니다.
-
LLM의 기능 확장 필요성
- 앞서 언급했던 LLM의 태생적 한계와 관련 있습니다.
- LLM은 강력한 자연어 처리 능력을 가지고 있지만, 자체적으로는 외부 세계와 직접 상호작용하거나 특정 작업을 수행할 수 없습니다. 예를 들어, 파일을 읽거나, 데이터베이스를 조회하거나, 외부 API를 호출하는 등의 작업은 LLM 자체의 기능만으로는 불가능합니다.
-
데이터 및 도구 통합의 복잡성
- LLM 기반 애플리케이션을 개발할 때, 다양한 데이터 소스 (파일 시스템, 데이터베이스, 웹 서비스 등)와 도구 (검색 엔진, 코드 실행 환경 등)를 LLM과 통합해야 하는 경우가 많습니다.
- 이러한 통합 과정은 복잡하고 번거로우며, 각 애플리케이션마다 개별적으로 구현해야 하는 문제가 있습니다.
-
표준화된 인터페이스 부재
- LLM과 외부 리소스 간의 상호 작용 방식을 정의하는 표준화된 프로토콜이 없기 때문에, 애플리케이션 개발자와 LLM 제공자 간의 협업이 어렵고, 개발된 애플리케이션의 재사용성 및 호환성이 떨어지는 문제가 있습니다.
-
보안 및 권한 관리
- LLM이 외부 리소스에 접근할 때, 보안 및 권한 관리가 중요합니다. 민감한 정보에 대한 접근을 제어하고, LLM이 수행할 수 있는 작업의 범위를 제한하는 메커니즘이 필요합니다.
MCP는 이러한 문제점을 해결하기 위해 등장했습니다. MCP는 "AI 애플리케이션을 위한 USB-C 포트"와 같이, LLM 애플리케이션과 외부 리소스 간의 표준화된 연결 방식을 제공합니다.
이를 통해:
- LLM은 MCP 서버를 통해 다양한 데이터와 도구에 접근할 수 있게 됩니다.
- 개발자는 재사용 가능한 MCP 서버를 쉽게 구축하고 공유할 수 있습니다.
- 클라이언트는 MCP를 지원함으로써 다양한 서버와 상호작용 할 수 있습니다.
- LLM 애플리케이션의 개발 및 배포가 간소화됩니다.
- 보안 및 권한 관리가 용이해집니다.
MCP 구성 요소
MCP를 구성하는 3대 요소에 대해 알아보겠습니다.
1. Host Application
LLM 애플리케이션 (예: Claude Desktop, IDE) 또는 AI 도구와 같이 MCP를 통해 LLM 기능을 활용하고자 하는 주체입니다.
- 사용자와 직접 상호작용하는 인터페이스를 제공합니다.
- 하나 이상의 MCP Client를 초기화하고 관리합니다.
- MCP Client를 통해 MCP Server에 연결하고, 요청을 보내고, 응답을 받습니다.
- LLM과의 상호작용을 시작하고, 사용자에게 결과를 제공합니다.
구체적인 예시로는 Claude desktop, Cursor 같은 것을 들 수 있겠습니다. 사용자(고객) 입장에서 마주치는 유일한 접점이고 서비스 개발자 입장에서는 건드리지 않는 영역입니다.
2. MCP Client
Host Application 내부에 존재하며, MCP Server와의 1:1 연결을 관리하는 컴포넌트입니다.
- MCP Server와의 연결 관리
- MCP Client는 하나 이상의 MCP Server와 1:1 연결을 설정하고 유지하는 역할을 합니다. 이 연결을 통해 MCP Server가 제공하는 리소스, 도구, 프롬프트 등에 접근할 수 있습니다.
- Protocol 처리
- MCP Client는 Model Context Protocol에 정의된 메시지 형식, 요청/응답 패턴, 오류 처리 등을 구현합니다. 이를 통해 MCP Server와 표준화된 방식으로 통신합니다.
- Host Application과의 통합
- MCP Client는 Host Application (예: Claude Desktop, IDE, 기타 AI 도구) 내부에 통합되어, Host Application이 MCP Server의 기능을 활용할 수 있도록 인터페이스를 제공합니다.
서비스 개발자로서 개발할 수 있는 요소 중에 하나입니다.
3. MCP Server
LLM에게 제공할 수 있는 특정 기능 또는 데이터 소스를 캡슐화하는 경량 프로그램입니다.
- Model Context Protocol을 준수하는 인터페이스를 제공합니다.
- Resources (데이터), Tools (기능), Prompts (템플릿)를 Clients에게 노출합니다.
- Clients로부터 요청을 받아 처리하고, 결과를 반환합니다.
- 자체적으로 로컬 데이터 소스 (파일, 데이터베이스 등)에 접근하거나, 외부 서비스 (API)와 연동할 수 있습니다.
MCP server가 MCP client에게 노출하는 주요 항목 3가지에 대해 좀 더 알아보겠습니다.
Server ➡️ Client: Tools
MCP Server가 제공하는 실행 가능한 기능 입니다. LLM은 Tools를 호출하여 특정 작업을 수행하거나 외부 시스템과 상호작용할 수 있습니다.
- 핵심 특징
- Model-controlled: Tools는 LLM에 의해 자동으로 호출됩니다 (물론, 사용자의 승인 하에). 즉, LLM이 주어진 context와 task를 기반으로 어떤 Tool을 호출할지 결정합니다.
- 동작 기반: Tools는 단순한 데이터 제공을 넘어, 동작(action)을 수행합니다. 예를 들어, 파일 시스템을 조작하거나, 데이터베이스를 쿼리하거나, 외부 API를 호출하는 등의 작업을 수행할 수 있습니다.
- JSON Schema 기반 정의: 각 Tool은 JSON Schema를 사용하여 입력 매개변수(input schema)를 명확하게 정의합니다. 이를 통해 LLM은 Tool의 사용법을 이해하고 올바른 형식으로 호출할 수 있습니다.
- 동작 방식
- Discovery (발견): Client는 tools/list 요청을 통해 Server가 제공하는 Tool 목록과 각 Tool의 input schema를 얻습니다.
- Invocation (호출): LLM은 특정 Tool을 호출하기로 결정하고, Client에게 tools/call 요청을 보냅니다. 이 요청에는 Tool 이름과 JSON Schema에 맞는 입력 매개변수가 포함됩니다.
- Execution (실행): Server는 해당 Tool에 대한 로직을 실행하고, 결과를 Client에게 반환합니다.
- Error Handling (오류 처리): Tool 실행 중 오류가 발생하면, Server는 오류 정보를 포함한 응답을 반환합니다.
- 예시
- execute_command: 쉘 명령어를 실행하는 Tool
- github_create_issue: GitHub에 이슈를 생성하는 Tool
- analyze_csv: CSV 파일을 분석하는 Tool
- calculate_sum: 두 숫자를 더하는 Tool (간단한 예시)
- get_forecast: 위도, 경도 기반으로 날짜 정보를 조회하는 Tool 👈 이 Tool을 직접 구현해볼 예정입니다!
Server ➡️ Client: Resources
MCP Server가 제공하는 데이터 입니다. 파일, 데이터베이스 레코드, API 응답, 스크린샷, 로그 파일 등 다양한 형태의 데이터가 Resource가 될 수 있습니다.
-
핵심 특징
- Application-controlled: Resources는 Client 애플리케이션에 의해 명시적으로 선택되어 LLM에게 제공됩니다. 즉, Client 애플리케이션이 어떤 Resource를 LLM에게 context로 제공할지 결정합니다.
- URI 기반 식별: 각 Resource는 고유한 URI (Uniform Resource Identifier)로 식별됩니다.
- Text 또는 Binary: Resources는 텍스트 데이터(UTF-8) 또는 바이너리 데이터(base64 인코딩)를 포함할 수 있습니다.
- URI Templates: 동적인 Resource의 경우, URI Template (RFC 6570)을 사용하여 Client가 유효한 URI를 생성할 수 있도록 합니다.
-
동작 방식
-
Discovery (발견): Client는 resources/list 요청을 통해 Server가 제공하는 Resource 목록 (또는 URI Template 목록)을 얻습니다.
-
Read (읽기): Client는 특정 Resource의 URI를 사용하여 resources/read 요청을 보내고, Server는 해당 Resource의 내용을 반환합니다.
-
Updates (업데이트): Server는 notifications/resources/list_changed (Resource 목록 변경) 또는 notifications/resources/updated (특정 Resource 내용 변경) 알림을 통해 Client에게 Resource의 변경 사항을 알릴 수 있습니다.
-
-
예시
- file:///home/user/documents/report.pdf (파일)
- postgres://database/customers/schema (데이터베이스 스키마)
- screen://localhost/display1 (스크린샷)
- logs://recent?timeframe=1h (로그 파일, URI Template 사용)
Server ➡️ Client: Prompts
MCP Server가 제공하는 재사용 가능한 프롬프트 템플릿 입니다. Prompts는 LLM에게 특정 작업을 지시하거나, 특정 형식으로 응답하도록 유도하는 데 사용됩니다.
- 핵심 특징
- User-controlled: Prompts는 사용자가 명시적으로 선택하여 사용하는 것을 의도합니다. 즉, Client는 Server로부터 Prompt 목록을 가져와 사용자에게 보여주고, 사용자가 선택한 Prompt를 실행하는 방식으로 동작합니다.
- 동적 인수: Prompts는 동적 인수를 가질 수 있습니다. 예를 들어, "코드 분석" Prompt는 분석할 코드 파일의 경로를 인수로 받을 수 있습니다.
- Multi-step workflows: Prompts는 여러 단계의 LLM 상호작용을 포함할 수 있습니다. (예: "오류 디버깅" Prompt는 먼저 오류 메시지를 입력받고, LLM에게 분석을 요청하고, 사용자의 추가 입력을 받는 등의 단계를 포함할 수 있습니다.)
- UI Integration: Prompts는 Client UI에서 슬래시 명령어, 빠른 실행 메뉴, 컨텍스트 메뉴 등의 형태로 사용자에게 제공될 수 있습니다.
- 동작 방식
- Discovery (발견): Client는 prompts/list 요청을 통해 Server가 제공하는 Prompt 목록을 얻습니다.
- Get (가져오기): Client는 사용자가 선택한 Prompt의 이름과 인수를 포함하여 prompts/get 요청을 보냅니다.
- Execution (실행): Server는 해당 Prompt에 정의된 메시지 템플릿을 기반으로 실제 메시지를 생성하여 Client에게 반환합니다. 이 메시지에는 Resource의 내용이 포함될 수도 있습니다.
- 예시
- git-commit: Git 커밋 메시지를 생성하는 Prompt
- explain-code: 코드를 설명하는 Prompt
- analyze-project: 프로젝트 로그와 코드를 분석하는 Prompt (Resource 포함)
- debug-error: 오류를 디버깅하는 Prompt (Multi-step workflow)
MCP server의 각 구성 요소에 대해 정리하자면 다음과 같습니다:
Feature | Tools | Resources | Prompts |
---|---|---|---|
Purpose | 실행 가능한 기능 제공 | 접근 가능한 데이터 제공 | 재사용 가능한 프롬프트 템플릿 제공 |
Control | Model-controlled (자동) | Application-controlled (명시적) | User-controlled (명시적) |
Identification | 고유한 이름 | URI | 고유한 이름 |
Input | JSON Schema로 정의된 매개변수 | URI (또는 URI Template) | 동적 인수 |
Output | 실행 결과 (텍스트, 이미지, 오류 등) | 텍스트 또는 바이너리 데이터 | 완성된 메시지 (LLM에게 보낼 준비가 된) |
기술적 구현 방법
여기까지 오느라 정말 수고 많으셨습니다! 이제 실제 코드를 보면서 구체적인 구현 방법에 대해 설명하겠습니다.
이제 MCP 공식 문서에서도 제공하는 예제 중의 하나인 Weather
use case를 Node로 구현해봅시다.
System requirements
개발할 로컬 컴퓨터에 node
가 설치되어 있어야 합니다. 가급적 최신 버전이어야 하고 만약 설치되어있지 않다면 nodejs.org를 방문해서 설치해야 합니다.
Setup
이제 적당한 작업 디렉토리를 생성해야 합니다.
cd {your workspace directory}
mkdir weather # `md weather` for Windows
cd weather
# Initialize a new npm project
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
# Create our files
mkdir src # `md src` for Windows
touch src/index.ts # `new-item src\index.ts` for Windows
- 이 예제를 위해 총 4개의 dependency를 설치합니다.
- @modelcontextprotocol/sdk: MCP 라이브러리
- zod: Tool의 Parameter 지정할 때 사용되는 schema 선언 라이브러리
- @types/node, typescript: 애플리케이션 빌드와 구동할 때 필요한 라이브러리
{
"type": "module",
"bin": {
"weather": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
},
"files": [
"build"
],
}
npm init -y
명령어를 통해 생성된 package.json 파일을 위와 같이 수정합니다.- Line 7:
tsc
명령어로 typescript 파일 빌드를 한 후에build/index.js
파일의 권한을 변경합니다
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
weather
디렉토리에 tsconfig.json 파일을 생성한 후에 위 내용을 추가합니다.
Source code - MCP server
프로젝트 설정이 모두 완료되었습니다. 이제 코드 작업을 할 차례입니다.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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",
});
- src 디렉토리 하위에
index.ts
파일을 생성하고 위 코드를 입력하세요. - Line 9-12:
weather
라는 이름의 MCP server 인스턴스를 생성합니다.
이제 Helper function 들을 추가해야 합니다. 아래 코드를 방금 생성한 index.ts 파일에 추가하세요.
// 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[];
};
}
makeNWSRequest
- National Weather Service (NWS) API에 HTTP GET 요청을 보내고, 응답을 JSON 형태로 파싱하여 반환
formatAlert
- NWS API로부터 받은 경고(Alert) 데이터 중 하나의 feature 객체를 사람이 읽기 쉬운 문자열 형태로 변환
그러면 이제 제일 핵심적인 tool execution handler 코드를 추가해봅시다. 마찬가지로 아래 코드를 src/index.ts 파일에 추가하세요.
// Register weather tools
server.tool(
"get-alerts", // Tool name
"Get weather alerts for a state", // Tool description
{
state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"),
}, // input parameters (Zod schema)
async ({ state }) => { // Tool logic (async function)
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 === 0) {
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.tool(
"get-forecast",
"Get weather forecast for a location",
{
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,
},
],
};
},
);
- Line 2, 50: 앞서 생성한 server 인스턴스의
tool
함수를 호출하여 Tool을 등록합니다. tool 함수는 총 4개의 인자를 받습니다.- Tool name: Tool의 고유한 이름. MCP Server 내에서 유일해야 합니다.
- Tool description(optional): Tool의 기능에 대한 간결하고 명확한 설명. 이 설명은 Client (및 LLM)에게 Tool의 용도를 알려주는 데 사용됩니다.
- Input parameter schema: Tool의 입력 매개변수를 정의하는 Zod 스키마
- Tool logic: Tool이 호출될 때 실행될 실제 로직을 담고 있는 콜백 함수. Input parameter schema를 그대로 함수 인자로 받습니다.
get-forecast
tool- Line 51: Tool name
- Line 52: Tool description
- Line 53-56: Input parameter schema. 2개의 인자 (
latitude
,longitude
) 각각에 대한 스키마를 정의. 인자 별로도 describe 함수를 통해 각 인자가 어떤 의미인지를 지정하고 이것이 LLM에게 전달됩니다. - Line 57-131: Tool logic. latitude, longitude 기반으로 NWS API를 호출하여 해당 지역의 날씨 예측 정보를 가져옵니다. 날씨 예측 정보를 적절히 파싱하여 가장 마지막에 return 합니다. 이 return 데이터는 LLM에게 제공하는 Context가 됩니다. 결국에 이 데이터 제일 핵심이라고 볼 수 있습니다.
Tool 등록이 끝났고 이제 MCP server를 실행시키는 코드를 마지막으로 추가해야 합니다.
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);
});
- MCP server는 client와 통신을 위한 2가지 전송 프로토콜(Transport Layer)을 지원합니다.
- Stdio (Standard Input/Output)
- HTTP with SSE (Server-Sent Events)
- Line 2: 이 예제에서는 Stdio를 사용
Build - MCP server
아래 명령어를 통해 src/index.ts 파일을 빌드합니다.
npm run build
- 그러면
build
디렉토리가 새로 생기고 그 안에index.js
파일이 생긴 것을 볼 수 있습니다.
Claude Desktop configuration
이제 모든 코드 작업은 완료되었습니다. 우리가 작성한 MCP server를 Claude Desktop과 연동하여 테스트 해볼 차례입니다.
로컬 컴퓨터에 Claude Desktop이 설치되어 있어야 합니다. 만약 설치되어 있지 않다면 이곳에서 다운로드 가능합니다.
MCP server와 Claude를 연결시키기 위해서는 Claude App configuration 파일을 수정해야 합니다.
~/Library/Application Support/Claude/claude_desktop_config.json
이 경로의 파일을 열어서 아래 내용을 추가하세요.
{
"mcpServers": {
"weather": {
"command": "node",
"args": [
"/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather/build/index.js"
]
}
}
}
mcpServers
항목 하위에weather
라는 항목을 추가합니다. 이것은 우리가 위에서 만든 MCP server의 이름입니다.weather
항목 하위에command
와args
가 있습니다- command: weather server를
node
로 구동시킵니다 - args: weather server의 index.js 파일 위치. 상대 경로가 아니라 절대 경로를 지정해야 합니다.
- command: weather server를
Test MCP server with Claude Desktop
이제 정말 모든 준비가 다 끝났습니다. Claude Desktop을 실행해봅시다. (기존에 미리 실행중이었다면 종료 후 다시 실행해야 합니다)

- 모든 설정을 마친 후 Claude Desktop을 실행하면 채팅 메시지 input box 우하단에 망치 모양 아이콘이 새로 생겨난 것을 볼 수 있습니다.

- 해당 망치 아이콘을 클릭하면 Claude Desktop에 등록된 MCP
Tools
목록을 볼 수 있습니다.- 우리가 만든 2개의 Tool(
get-alerts
,get-forecast
)이 보입니다.
- 우리가 만든 2개의 Tool(
- 여기까지 정상적으로 진행되었다면, MCP server와 Claude Desktop을 성공적으로 연결시킨 것입니다!

What's the weather in Boston?
- 채팅창에 Boston의 날씨를 물어봤습니다
- LLM은 이 사용자 질의를 바탕으로 사용 가능한 Tool 중에 어떤 것을 호출할지 결정하고 적절한 Tool parameter를 지정하여 호출합니다
- 선택된 Tool은
get-forecast
이고 Parameter는latitude
: 42.36,longitude
: -71.06 입니다 - LLM은
Boston
의 위도, 경도를 알고 있기 때문에 get-forecast Tool을 호출하면서 적절한 Parameter를 지정할 수 있는 것입니다
- 선택된 Tool은
- get-forecast Tool의 Tool logic은 NWS API를 호출하고 응답 받은 날씨 예측 데이터를 리턴합니다
- LLM은 Tool logic이 리턴한 날씨 예측 데이터를 사람이 읽기 쉽게 풀어서 최종 응답을 생성합니다
아래 시퀀스 diagram을 통해 각 MCP 구성 요소가 순차적으로 어떻게 상호작용하는지 정리해보겠습니다.
- 사용자가 Host Application(Claude Desktop)의 UI와 상호작용하면, Host는 MCP Client를 통해 해당 요청을 MCP Server로 전달합니다.
- MCP Client는 MCP Protocol을 사용하여 Server에 요청을 보내고, Server는 요청 처리를 위해 필요한 경우 외부 데이터 소스(Data/API)에 접근합니다.
- Server는 데이터 소스로부터 얻은 결과 또는 처리 결과를 MCP Protocol 응답 형태로 Client에 반환합니다.
- Client는 응답을 Host Application에 다시 전달하고, Host Application은 최종 결과를 사용자에게 UI를 통해 표시하거나 업데이트합니다.
MCP를 통해 우리는 Boston의 오늘 날씨 정보를 알 수 있게 되었습니다. 이 글의 초반부에서 Seoul의 날씨를 물어보니 Claude가 응답할 수 없다고 했던 것과 대조되는 결과입니다.
어떻게 이런 차이가 생겼을까요?
LLM에게 적절한 Context를 제공할 수 있는가
이것이 핵심입니다. LLM에게 다음과 같은 요소들을 Context로 제공하는 것이 중요합니다:
- 사용 가능한 Tool의 이름과 종류
- Tool을 호출해야 하는 적절한 상황
- 호출 시 필요한 구체적인 스키마
- Tool logic을 통해 리턴받은 최종 데이터
이러한 Context를 종합적으로 제공함으로써 LLM이 만드는 최종 답변의 품질 차이를 만드는 것입니다.
LLM integration: 개발자 관점에서 봤을 때의 MCP
기존 LLM 기반의 서비스는 LLM과의 integration 방향이 단방향이었습니다. 자체적으로 개발하는 서버에서 LLM API를 호출하는 방식이었으니까요. 그런데 MCP를 통해서 이 통합 방향을 양방향으로 할 수 있게 되었습니다. 내가 개발한 서버를 MCP를 통해 LLM에 등록하고 LLM이 서버를 호출하여 사용할 수 있게 되었기 때문입니다.
기존 LLM integration
- 최초 User 요청을 받는 곳은 User interface(Front-end) 입니다. 이곳에서 Application server API를 호출합니다
- Application server는 요청의 종류에 따라 내부 Business logic을 수행하고 그 결과 LLM API Client가 호출될 수 있습니다
- LLM API Client 호출 시 파라미터(System prompt, 대화 이력, 추가 Context)를 지정하는 책임은 Application server에게 있습니다. 따라서 개발자 관점에서 봤을 때, LLM을 따로 관리해야 하는 비용이 발생합니다
- LLM API client는 LLM Provider(OpenAI, Anthropic, Google)에서 제공하는 API를 호출합니다
LLM integration with MCP
- 최초 User 요청을 받는 곳은 Host Application(Claude Desktop, Cursor 등등) 입니다.
- Host Application 내부의 MCP Client는 Host Application에 등록된 모든 MCP Server에 연결하고, 각 서버가 제공하는 기능 (Tools, Resources, Prompts) 을 LLM에게 통합적으로 제공합니다. LLM (Tools의 경우) 또는 사용자 (Prompts, Resources의 경우) 가 필요한 기능을 선택하면, MCP Client는 해당 기능을 제공하는 MCP Server에게 요청을 전달합니다
- MCP Server는 요청을 전달받아 처리합니다. 이 과정에서 Application Server와 통신할 수도 있습니다
- LLM에 추가적인 Context를 주입하는 책임은 Host Application에 있습니다
- 즉 개발자 입장에서는 MCP server를 구현함으로써 Context를 제공만 할 뿐입니다
지금까지의 내용을 제어의 역전 (Inversion of Control) 관점에서 정리해보겠습니다:
특징 | 기존 방식 (APP Server ➡️ LLM API) | MCP 방식 (LLM ➡️ APP Server) |
---|---|---|
제어 흐름 주체 | Application Server (개발자) | Host Application (MCP Client) |
API 호출 주도권 | Application Server | Host Application |
Context 주입 책임 | Application Server (개발자) | Host Application |
유연성/확장성 | 낮음: 기능 확장을 위해 Application Server 코드 직접 수정 필요 | 높음: MCP Server (기능) 추가/제거를 통해 유연하게 기능 확장 가능 |
재사용성/모듈성 | 낮음: 기능이 Application Server에 tightly coupled | 높음: MCP Server는 독립적인 모듈 형태로 개발되어 재사용성 높음 |
개발 복잡성 | 상대적으로 낮음: 제어 흐름 단순, 개발자가 모든 것 직접 제어 | 상대적으로 높음: IoC 패턴 이해 필요, MCP 아키텍처 및 프로토콜 이해 필요 |
디버깅/유지보수 | 상대적으로 쉬움: 제어 흐름 직관적, 디버깅 및 문제 해결 용이 | 상대적으로 어려움: 제어 흐름 복잡, 컴포넌트 간 상호 작용 이해 필요 |
다만, MCP 방식이 꼭 기존 방식보다 나은 것은 아니고 MCP 방식으로도 개발자가 직접 LLM API를 호출하는 것이 가능하기 때문에 이 비교를 모든 LLM integration에 다 적용할 수 있는 것이 아니라는 점을 강조합니다.
마치며
2024년 11월에 발표된 MCP는 시간이 지남에따라 점점 더 많은 곳에서 찾는 대세가 되어가고 있습니다. 기술적으로도 많은 곳에서 사용되고 있고 Claude 뿐만 아니라 Cursor, Docker, Puppeteer, Github, Redis, PostgreSQL과 같은 다양한 서비스와 통합되고 있는 중입니다.
MCP가 구체적으로 어떤 방식으로 사용되는지 찾을 수 있는 MCP client/server 들만 모아놓은 웹 페이지들이 있습니다:
위 registry 들을 보면 정말 수 많은 곳에서 MCP를 차용하고 있습니다. 이 중에는 개발자 경험과 생산성을 극대화 하는 것들도 있는데요. 이것들에 대해서는 다른 블로그 포스팅을 통해 상세하게 알아볼 계획입니다
그리고 본 글에서는 MCP의 구성 요소 중에 MCP server, 그 중에서도 Tools
만 구현하여 사용해봤습니다. 다른 MCP의 중요 항목에 대해서 더 구현해보고 싶은 분들은 MCP 공식 문서를 참조하시면 되겠습니다. MCP 공식 문서는 상당히 잘 쓰여져 있기 때문에 처음부터 천천히 읽어가면서 이해하는 것도 괜찮은 학습 방법입니다.
그러면 이번 글은 여기서 마치겠습니다. 끝까지 읽어주셔서 감사합니다!
Comments (0)
Checking login status...
No comments yet. Be the first to comment!