MCP(Model Context Protocol)
1. MCP(Model Context Protocol)
MCP는 2024년 11월에 앤트로픽에서 발표하였으며 애플리케이션이 LLM에 컨텍스트를 제공하는 방식을 표준화하는 개방형 프로토콜입니다. 앤트로픽은 MCP가 AI의 ‘USB-C’ 포트와 같다고 설명했습니다. USB-C가 다양한 주변 기기 및 액세서리에 기기를 연결하는 표준화된 방식을 제공하는 것처럼, MCP는 AI 모델을 다양한 데이터 소스 및 도구에 연결하는 표준화된 방식을 제공합니다.
MCP 사용 이유
- LLM을 기반으로 에이전트와 복잡한 워크플로를 구축하는 데 도움을 줍니다.
- LLM은 데이터 및 도구와 통합해야 하는 경우가 많으며, 직접 연결할 수 있는 미리 구축된 통합 목록이 점점 늘어나고 있는 상황에서 유용할 것으로 보인다.
2. Core Architecture
MCP는 본질적으로 호스트 애플리케이션이 여러 서버에 연결할 수 있는 클라이언트-서버 아키텍처를 따릅니다.
- MCP 호스트 : MCP를 통해 데이터에 액세스하려는 Claude Desktop, IDE 또는 AI 도구와 같은 프로그램
- MCP 클라이언트 : 서버와 1:1 연결을 유지하는 프로토콜 클라이언트
- MCP 서버 : 표준화된 모델 컨텍스트 프로토콜을 통해 각각 특정 기능을 노출하는 경량 프로그램
- 로컬 데이터 소스 : MCP 서버가 안전하게 액세스할 수 있는 컴퓨터의 파일, 데이터베이스 및 서비스
- 원격 서비스 : MCP 서버가 연결할 수 있는 인터넷(예: API를 통해)을 통해 사용 가능한 외부 시스템
2-1. 개요
- 호스트: 연결을 시작하는 LLM 애플리케이션(예: Claude Desktop 또는 IDE)입니다.
- 클라이언트: 호스트 애플리케이션 내부에서 서버와 1:1 연결을 유지합니다.
- 서버: 클라이언트에게 컨텍스트, 도구 및 프롬프트를 제공합니다.
2-2. 핵심 구성 요소
프로토콜 계층
프로토콜 계층은 메시지 프레이밍, 요청/응답 연결, 고수준 통신 패턴을 처리합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Protocol<Request, Notification, Result> {
// Handle incoming requests
setRequestHandler<T>(schema: T, handler: (request: T, extra: RequestHandlerExtra) => Promise<Result>): void
// Handle incoming notifications
setNotificationHandler<T>(schema: T, handler: (notification: T) => Promise<void>): void
// Send requests and await responses
request<T>(request: Request, schema: T, options?: RequestOptions): Promise<T>
// Send one-way notifications
notification(notification: Notification): Promise<void>
}
전송 계층
전송 계층은 클라이언트와 서버 간의 실제 통신을 처리합니다.
전송 메커니즘
- Stdio 전송
- 통신을 위해 표준 입출력을 사용합니다.
- 로컬 프로세스에 이상적
- SSE 전송을 통한 HTTP
- 서버-클라이언트 메시지에 대해 서버에서 보낸 이벤트를 사용합니다.
- 클라이언트-서버 메시지에 대한 HTTP POST
- 모든 전송은 JSON-RPC 2.0을 사용하여 메시지를 교환
메시지 유형
- 요청: Request
- 결과: Result
- 오류: Error
- 알림: Notification
2-3. 연결 수명주기
초기화
- initialize클라이언트는 프로토콜 버전 및 기능과 함께 요청을 보냅니다 .
- 서버는 프로토콜 버전과 기능으로 응답합니다.
- initialized클라이언트가 확인으로 알림을 보냅니다 .
- 정상적인 메시지 교환이 시작됩니다.
메시지 교환
- 요청-응답 : 클라이언트 또는 서버가 요청을 보내고 다른 쪽이 응답합니다.
- 알림 : 양측이 일방적으로 메시지를 보냅니다.
종료
어느 당사자든 연결을 종료할 수 있습니다.
3. Resources
리소스는 MCP의 핵심 요소로, 서버가 클라이언트가 읽고 LLM 상호 작용에 대한 컨텍스트로 사용할 수 있는 데이터와 콘텐츠를 노출할 수 있도록 해줍니다.
리소스 종류
- 파일 내용
- 데이터베이스 레코드
- API 응답
- 스크린샷 및 이미지
리소스 URI
리소스는 다음 형식을 따르는 URI를 사용하여 식별됩니다.
프로토콜과 경로 구조는 MCP 서버 구현에 의해 정의됩니다. 서버는 자체적인 사용자 지정 URI 체계를 정의할 수 있습니다.
리소스 유형
- 텍스트 리소스: UTF-8로 인코딩된 텍스트 데이터(ex: 소스 코드, JSON/XML 데이터)
- 바이너리 리소스: base64로 인코딩된 원시 바이너리 데이터(ex: 이미지, PDF, 오디오 파일, 비디오 파일)
클라이언트-사용 가능한 리소스 찾기
- Direct resources:서버는 resources/list 엔드포인트를 통해 구체적인 리소스 목록을 제공합니다
- Resource templates: 동적 리소스의 경우 서버는 클라이언트가 유효한 리소스 URI를 구성하는 데 사용할 수 있는 URI 템플릿을 노출할 수 있습니다.
클라이언트-리소스 읽기
리소스를 읽으려면 클라이언트가 resources/read리소스 URI로 요청을 합니다.
{
contents: [
{
uri: string; // The URI of the resource
mimeType?: string; // Optional MIME type
// One of:
text?: string; // For text resources
blob?: string; // For binary resources (base64 encoded)
}
]
}
Flow
- 클라이언트 → 서버에 “리소스 목록 주세요” 요청
- 서버 → file:///logs/app.log 이라는 리소스 목록 응답
- 클라이언트 → “그 로그 파일 읽어주세요” 요청
- 서버 → 실제 파일 내용 읽어서 반환
4. Prompts
프롬프트를 사용하면 서버가 재사용 가능한 프롬프트 템플릿과 워크플로를 정의하여 클라이언트가 사용자와 LLM에 쉽게 제공할 수 있습니다.
MCP 프롬프트 템플릿
- 동적 인수 허용
- 리소스의 컨텍스트 포함
- 여러 상호 작용 연결
- 특정 워크플로 가이드
- UI 요소(슬래시 명령 등)로 표면화
- 동적 프롬프트 생성 가능
클라이언트-프롬프트 찾기
클라이언트는 prompts/list 엔드포인트를 통해 사용 가능한 프롬프트를 찾을 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Request
{
method: "prompts/list"
}
// Response
{
prompts: [
{
name: "analyze-code",
description: "Analyze code for potential improvements",
arguments: [
{
name: "language",
description: "Programming language",
required: true
}
]
}
]
}
클라이언트-프롬프트 사용
프롬프트를 사용하려면 클라이언트가 prompts/get요청합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Request
{
method: "prompts/get",
params: {
name: "analyze-code",
arguments: {
language: "python"
}
}
}
// Response
{
description: "Analyze Python code for potential improvements",
messages: [
{
role: "user",
content: {
type: "text",
text: "Please analyze the following Python code for potential improvements:\n\n```python\ndef calculate_sum(numbers):\n total = 0\n for num in numbers:\n total = total + num\n return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n```"
}
}
]
}
5. Tools
도구는 MCP의 강력한 기본 요소로, 서버가 클라이언트에 실행 가능한 기능을 노출할 수 있도록 합니다. 도구를 통해 LLM은 외부 시스템과 상호 작용하고, 계산을 수행하고, 실제 환경에서 작업을 수행할 수 있습니다.
도구의 주요 기능
- 발견 : 클라이언트는 tools/list엔드포인트를 통해 사용 가능한 도구를 나열할 수 있습니다.
- 호출 : 도구는 tools/call서버가 요청된 작업을 수행하고 결과를 반환하는 엔드포인트를 사용하여 호출됩니다.
- 유연성 : 도구는 간단한 계산부터 복잡한 API 상호 작용까지 다양합니다.
오류 처리 도구 오류는 MCP 프로토콜 수준 오류가 아닌 결과 객체 내에 보고되어야 합니다. 이를 통해 LLM이 오류를 확인하고 잠재적으로 처리할 수 있습니다.
- 결과 isError에 설정true
- content배열 에 오류 세부 정보 포함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
try {
// Tool operation
const result = performOperation();
return {
content: [
{
type: "text",
text: `Operation successful: ${result}`
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `Error: ${error.message}`
}
]
};
}
MCP 서버에 도구 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});
// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
description: "Add two numbers together",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
}
}]
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "calculate_sum") {
const { a, b } = request.params.arguments;
return {
content: [
{
type: "text",
text: String(a + b)
}
]
};
}
throw new Error("Tool not found");
});
6. Sampling
서버가 LLM으로부터 응답 생성을 요청할 수 있도록 지원합니다. 보안과 개인 정보 보호를 유지하면서 정교한 에이전트 동작을 가능하게 합니다.
샘플링 작동 방식
- sampling/createMessage서버가 클라이언트에게 요청을 보냅니다.
- 클라이언트는 요청을 검토하고 수정할 수 있습니다.
- LLM의 클라이언트 샘플
- 클라이언트가 완료를 검토합니다
- 클라이언트는 결과를 서버로 반환합니다.
요청 매개변수
messages: LLM으로 전송할 대화 기록이 포함
- role: “사용자” 또는 “보조자”
- content: 메시지 내용은 다음과 같습니다.
- text: 필드 가 있는 텍스트 콘텐츠
- data: base64 및 mimeType필드가 포함된 이미지 콘텐츠
modelPreferences: 서버가 모델 선택 기본 설정을 지정
- hints: 클라이언트가 적절한 모델을 선택하는 데 사용할 수 있는 다양한 모델 이름 제안:
- name: 전체 또는 일부 모델 이름과 일치할 수 있는 문자열(예: “claude-3”, “sonnet”)
- costPriority: 비용 최소화의 중요성
- speedPriority: 저지연 응답의 중요성
- intelligencePriority: 고급 모델 기능의 중요성
systemPrompt: 서버는 특정 시스템 프롬프트를 요청
includeContext: 포함할 MCP 컨텍스트 지정
- “none”: 추가 컨텍스트 없음
- “thisServer”: 요청 서버의 컨텍스트를 포함합니다.
- “allServers”: 연결된 모든 MCP 서버의 컨텍스트 포함
샘플링 매개변수: LLM 샘플링을 미세 조정
- temperature: 무작위성 제어(0.0~1.0)
- maxTokens: 생성할 최대 토큰
- stopSequences: 생성을 중지하는 시퀀스 배열
- metadata: 추가 공급자별 매개변수
요청 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "What files are in the current directory?"
}
}
],
"systemPrompt": "You are a helpful file system assistant.",
"includeContext": "thisServer",
"maxTokens": 100
}
}
응답 예시
1
2
3
4
5
6
7
8
9
10
11
{
model: string, // Name of the model used
stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string,
role: "user" | "assistant",
content: {
type: "text" | "image",
text?: string,
data?: string,
mimeType?: string
}
}
7. Transports
Transports는 클라이언트와 서버 간 통신의 기반을 제공합니다. 전송은 메시지를 송수신하는 기본적인 메커니즘을 처리합니다. MCP는 JSON-RPC 2.0을 와이어 형식으로 사용합니다. 전송 계층은 MCP 프로토콜 메시지를 전송을 위해 JSON-RPC 형식으로 변환하고, 수신된 JSON-RPC 메시지를 다시 MCP 프로토콜 메시지로 변환하는 역할을 합니다.
JSON-RPC 메시지의 3가지 유형
요청
1
2
3
4
5
6
{
jsonrpc: "2.0",
id: number | string,
method: string,
params?: object
}
응답
1
2
3
4
5
6
7
8
9
10
{
jsonrpc: "2.0",
id: number | string,
result?: object,
error?: {
code: number,
message: string,
data?: unknown
}
}
알림
1
2
3
4
5
{
jsonrpc: "2.0",
method: string,
params?: object
}
전송 유형
표준 입출력(stdio): 표준 입출력 스트림을 통한 통신
1
2
3
4
5
6
7
8
9
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {}
});
const transport = new StdioServerTransport();
await server.connect(transport);
서버 전송 이벤트(SSE): 클라이언트-서버 통신을 위한 HTTP POST 요청을 통해 서버-클라이언트 스트리밍을 가능
SSE 전송은 적절하게 보안되지 않으면 DNS 리바인딩 공격에 취약할 수 있습니다.
DNS 리바인딩 공격 예방
- 예상 소스에서 왔는지 확인하기 위해 들어오는 SSE 연결에서 항상 Origin 헤더를 검증합니다.
- 로컬로 실행할 때 서버를 모든 네트워크 인터페이스 (0.0.0.0)에 바인딩하지 마세요. 대신 로컬호스트(127.0.0.1)에만 바인딩하세요.
- 모든 SSE 연결에 대해 적절한 인증을 구현합니다.
예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import express from "express";
const app = express();
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {}
});
let transport: SSEServerTransport | null = null;
app.get("/sse", (req, res) => {
transport = new SSEServerTransport("/messages", res);
server.connect(transport);
});
app.post("/messages", (req, res) => {
if (transport) {
transport.handlePostMessage(req, res);
}
});
app.listen(3000);