MCP의 JSON-RPC 2.0 기반 메시지 형식, 연결 생명주기, 능력 협상 메커니즘을 상세히 분석합니다.
MCP는 JSON-RPC 2.0을 메시지 형식으로 채택하고 있습니다. JSON-RPC는 JSON으로 인코딩된 원격 프로시저 호출(Remote Procedure Call) 프로토콜로, 단순하면서도 구조화된 요청-응답 패턴을 제공합니다.
MCP가 gRPC나 GraphQL 대신 JSON-RPC를 선택한 이유는 명확합니다. JSON-RPC는 프로토콜 자체가 매우 단순하여 다양한 전송 계층(stdio, HTTP, WebSocket) 위에서 동작할 수 있으며, 양방향 통신을 자연스럽게 지원합니다. MCP에서는 클라이언트가 서버에 요청을 보내는 것뿐 아니라, 서버가 클라이언트에 요청을 보내는 역방향 통신도 필요하기 때문입니다.
MCP에서 교환되는 모든 메시지는 세 가지 유형 중 하나에 해당합니다.
한쪽에서 다른 쪽에 특정 작업을 요청하는 메시지입니다. 반드시 응답이 돌아와야 합니다.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"city": "서울"
}
}
}jsonrpc: 항상 "2.0"입니다.id: 요청을 식별하는 고유 값입니다. 응답에서 동일한 id를 사용하여 어떤 요청에 대한 응답인지 매칭합니다.method: 호출할 메서드의 이름입니다.params: 메서드에 전달할 매개변수입니다.요청에 대한 결과를 반환하는 메시지입니다. 성공과 실패 두 가지 형태가 있습니다.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "서울의 현재 날씨: 맑음, 18도"
}
]
}
}{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params",
"data": "city 매개변수는 필수입니다."
}
}응답을 기대하지 않는 단방향 메시지입니다. id 필드가 없는 것이 특징입니다.
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}알림은 주로 상태 변경을 통보하는 데 사용됩니다. 예를 들어 서버의 도구 목록이 변경되었을 때, 진행 상황을 보고할 때, 로그 메시지를 전달할 때 알림을 보냅니다.
MCP 클라이언트와 서버 간의 연결은 명확한 생명주기를 따릅니다.
연결이 수립된 후 가장 먼저 수행되는 것은 초기화 핸드셰이크입니다.
클라이언트가 initialize 요청을 보내며, 이 요청에는 클라이언트의 정보와 지원하는 프로토콜 버전, 클라이언트의 능력(Capabilities)이 포함됩니다.
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {},
"elicitation": {}
},
"clientInfo": {
"name": "my-mcp-client",
"version": "1.0.0"
}
}
}서버는 자신의 정보와 능력을 응답으로 반환합니다.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": { "listChanged": true }
},
"serverInfo": {
"name": "weather-server",
"version": "2.1.0"
}
}
}클라이언트는 응답을 확인한 후 initialized 알림을 보내 초기화 완료를 통보합니다. 이 과정이 능력 협상(Capability Negotiation)입니다.
초기화가 완료되면 클라이언트와 서버는 자유롭게 메시지를 교환할 수 있습니다. 이 단계에서 실제 도구 호출, 리소스 읽기, 프롬프트 실행 등이 이루어집니다.
연결을 종료할 때는 클라이언트가 서버에 종료를 요청하고, 서버가 정리 작업을 수행한 뒤 응답합니다.
능력 협상은 MCP의 핵심 설계 원칙 중 하나입니다. 클라이언트와 서버가 서로의 기능을 동적으로 파악하여, 양쪽이 모두 지원하는 기능만 사용하도록 합니다.
서버가 선언할 수 있는 능력은 다음과 같습니다.
interface ServerCapabilities {
tools?: {
listChanged?: boolean; // 도구 목록 변경 알림 지원
};
resources?: {
subscribe?: boolean; // 리소스 구독 지원
listChanged?: boolean; // 리소스 목록 변경 알림 지원
};
prompts?: {
listChanged?: boolean; // 프롬프트 목록 변경 알림 지원
};
logging?: {}; // 로깅 지원
}서버가 tools 능력을 선언하지 않으면, 클라이언트는 해당 서버에 도구 관련 요청을 보내지 않습니다. 이를 통해 서버는 자신이 제공하는 기능의 범위를 명확히 정의할 수 있습니다.
클라이언트가 선언할 수 있는 능력은 다음과 같습니다.
interface ClientCapabilities {
roots?: {
listChanged?: boolean; // 루트 목록 변경 알림 지원
};
sampling?: {}; // 샘플링(LLM 호출) 지원
elicitation?: {}; // 유도(사용자 입력 요청) 지원
}샘플링(Sampling)은 서버가 클라이언트에 LLM 호출을 요청하는 기능입니다. 클라이언트가 이 능력을 선언하면, 서버는 자체적으로 LLM API를 호출하지 않고도 AI 추론을 활용할 수 있습니다.
유도(Elicitation)는 서버가 클라이언트를 통해 사용자에게 추가 정보를 요청하는 기능입니다. 예를 들어 데이터베이스 서버가 연결 정보를 사용자에게 물어볼 수 있습니다.
능력 협상은 하위 호환성을 보장하는 핵심 메커니즘입니다. 새로운 프로토콜 버전에 기능이 추가되더라도, 해당 기능을 지원하지 않는 구현체는 단순히 해당 능력을 선언하지 않으면 됩니다. 이를 통해 서로 다른 버전의 클라이언트와 서버가 안전하게 통신할 수 있습니다.
MCP 프로토콜에서 정의하는 주요 메서드를 역할별로 정리하면 다음과 같습니다.
| 메서드 | 설명 | 유형 |
|---|---|---|
initialize | 연결 초기화 및 능력 협상 | 요청 |
initialized | 초기화 완료 통보 | 알림 |
tools/list | 사용 가능한 도구 목록 조회 | 요청 |
tools/call | 특정 도구 실행 | 요청 |
resources/list | 사용 가능한 리소스 목록 조회 | 요청 |
resources/read | 특정 리소스 읽기 | 요청 |
resources/subscribe | 리소스 변경 구독 | 요청 |
prompts/list | 사용 가능한 프롬프트 목록 조회 | 요청 |
prompts/get | 특정 프롬프트 가져오기 | 요청 |
completion/complete | 자동완성 요청 | 요청 |
ping | 연결 상태 확인 | 요청 |
| 메서드 | 설명 | 유형 |
|---|---|---|
sampling/createMessage | LLM 호출 요청 | 요청 |
elicitation/create | 사용자 입력 요청 | 요청 |
roots/list | 루트 디렉토리 목록 요청 | 요청 |
notifications/tools/list_changed | 도구 목록 변경 통보 | 알림 |
notifications/resources/list_changed | 리소스 목록 변경 통보 | 알림 |
notifications/resources/updated | 리소스 내용 변경 통보 | 알림 |
notifications/progress | 진행 상황 보고 | 알림 |
실제 도구 호출이 프로토콜 수준에서 어떻게 이루어지는지 단계별로 살펴보겠습니다.
tools/call 요청을 보냅니다.이 과정에서 LLM은 MCP 프로토콜의 존재를 알 필요가 없습니다. LLM은 도구의 이름, 설명, 매개변수 스키마만 알면 되고, 실제 통신은 MCP 클라이언트와 서버가 처리합니다.
MCP 사양은 날짜 기반 버전 관리를 사용합니다. 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25 등의 형식입니다.
초기화 단계에서 클라이언트가 자신이 지원하는 프로토콜 버전을 선언하면, 서버는 해당 버전 또는 호환 가능한 버전으로 응답합니다. 서버가 클라이언트의 버전을 지원하지 않으면 에러를 반환하여 연결을 거부합니다.
function checkVersionCompatibility(
clientVersion: string,
serverVersion: string
): boolean {
const supported = ["2025-11-25", "2025-06-18", "2025-03-26"];
return supported.includes(clientVersion) && supported.includes(serverVersion);
}프로토콜 버전이 다른 클라이언트와 서버를 연결할 때는 주의가 필요합니다. 최신 버전의 기능(예: Elicitation, Tasks)은 해당 기능을 지원하지 않는 이전 버전의 구현체에서는 사용할 수 없습니다. 능력 협상을 통해 양쪽이 공통으로 지원하는 기능만 사용하도록 설계해야 합니다.
MCP는 JSON-RPC 2.0의 에러 코드 체계를 따르면서, MCP 전용 에러 코드를 추가로 정의합니다.
| 코드 | 이름 | 설명 |
|---|---|---|
| -32700 | Parse error | JSON 파싱 실패 |
| -32600 | Invalid request | 유효하지 않은 요청 |
| -32601 | Method not found | 존재하지 않는 메서드 |
| -32602 | Invalid params | 유효하지 않은 매개변수 |
| -32603 | Internal error | 서버 내부 오류 |
| 코드 | 이름 | 설명 |
|---|---|---|
| -32001 | Resource not found | 요청한 리소스 없음 |
| -32002 | Tool not found | 요청한 도구 없음 |
에러 응답에는 선택적으로 data 필드를 포함하여 추가 정보를 제공할 수 있습니다. 이를 활용하면 클라이언트가 에러의 원인을 파악하고 적절한 복구 조치를 취하는 데 도움이 됩니다.
server.tool("query_database", "데이터베이스를 쿼리합니다", {
query: { type: "string", description: "SQL 쿼리" },
}, async (params) => {
try {
const result = await db.query(params.query);
return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [{ type: "text", text: "쿼리 실행에 실패했습니다: " + error.message }],
isError: true,
};
}
});도구 실행 결과에서 isError: true를 설정하면, LLM이 이 결과를 에러로 인식하고 사용자에게 적절히 안내하거나 다른 접근 방식을 시도할 수 있습니다.
장시간 실행되는 도구의 경우, 서버는 진행 상황을 클라이언트에 실시간으로 보고할 수 있습니다. 이를 위해 요청 메시지에 _meta.progressToken을 포함시키고, 서버는 이 토큰을 사용하여 notifications/progress 알림을 보냅니다.
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "analyze_repository",
"arguments": { "url": "https://github.com/example/repo" },
"_meta": { "progressToken": "progress-1" }
}
}{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "progress-1",
"progress": 45,
"total": 100,
"message": "파일 분석 중... (450/1000)"
}
}JSON-RPC 2.0은 여러 요청을 하나의 배열로 묶어 보내는 배치 요청을 지원합니다. MCP에서도 이를 활용하여 네트워크 왕복을 줄일 수 있습니다.
[
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" },
{ "jsonrpc": "2.0", "id": 2, "method": "resources/list" },
{ "jsonrpc": "2.0", "id": 3, "method": "prompts/list" }
]서버는 각 요청에 대한 응답을 배열로 반환합니다. 응답의 순서는 요청의 순서와 일치하지 않을 수 있으므로, id 필드를 통해 매칭해야 합니다.
이 장에서 다룬 핵심 내용을 요약하면 다음과 같습니다.
3장에서는 MCP의 전송 계층(Transport Layer)을 다룹니다. stdio 전송과 Streamable HTTP 전송의 동작 원리, 각 전송 방식의 장단점, 그리고 상황에 따른 적절한 전송 방식 선택 기준을 살펴보겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
MCP의 두 가지 핵심 전송 방식인 stdio와 Streamable HTTP의 동작 원리, 장단점, 선택 기준을 상세히 다룹니다.
Model Context Protocol이 무엇이고, 왜 AI 생태계의 표준으로 자리 잡았는지, 그리고 이 시리즈에서 다룰 내용의 전체 지도를 살펴봅니다.
MCP 서버가 제공하는 세 가지 핵심 프리미티브의 스키마 정의, 구현 패턴, 실전 활용 사례를 상세히 다룹니다.