04. JSON-RPC 2.0 위에서 — 메시지 구조
- MCP 메시지의 세 종류(요청·응답·알림)를 구분해 읽고 쓸 수 있다
id가 왜 필요한지, 알림에는 왜id가 없는지 이해한다- JSON-RPC 표준 에러 객체와 주요 에러 코드를 안다
먼저: JSON-RPC가 뭔가요?
본격적인 비유에 앞서 용어부터 풀어 봅니다. RPC는 Remote Procedure Call, 즉 "원격 프로시저 호출"입니다. 내 프로그램에서 함수를 부르듯, 다른 곳에 있는 프로그램의 기능을 호출하는 방식을 말합니다. "이 함수를 이 인자로 실행해 줘 → 결과를 돌려줘"를 네트워크나 프로세스 경계 너머로 하는 것이죠.
JSON-RPC는 그 호출을 JSON(JavaScript Object Notation — {"key": "value"} 꼴의, 사람도 읽을 수 있는 보편적 데이터 표기법) 형식으로 표현하기로 약속한 규격입니다. 뒤의 2.0은 이 규격의 버전입니다. 정리하면 JSON-RPC 2.0은 "원격의 기능을 호출하고 결과를 받는 대화를, JSON이라는 공통 표기로 주고받자" 는 가볍고 언어 중립적인 약속입니다. 특정 회사나 언어의 것이 아니라 공개 표준이라, 파이썬 서버와 자바스크립트 클라이언트가 문제없이 대화할 수 있습니다.
MCP는 바로 이 JSON-RPC 2.0을 메시지 형식으로 채택했습니다. 그래서 이 장을 이해하면 MCP의 모든 대화를 읽을 수 있습니다.
우편물 비유
이제 비유로 직관을 잡아 봅시다. MCP의 모든 대화는 JSON-RPC 2.0이라는 편지 양식으로 오갑니다. 편지는 세 종류입니다.
- 요청(Request) = "답장 바랍니다"라고 쓴 등기우편. 받는 쪽은 반드시 답해야 합니다. 그래서 추적 번호(
id)가 붙습니다. - 응답(Response) = 그 등기우편에 대한 답장. 같은 추적 번호(
id)를 붙여 "어느 편지에 대한 답인지" 알려 줍니다. - 알림(Notification) = "읽어만 두세요"라고 쓴 일반우편. 답장이 필요 없으므로 추적 번호(
id)가 없습니다.
MCP는 JSON-RPC를 사용해 메시지를 인코딩하며, 모든 JSON-RPC 메시지는 반드시 UTF-8(전 세계 거의 모든 문자를 표현하는 표준 텍스트 인코딩 — 한글·이모지 포함)로 인코딩됩니다. 전송 방식이 stdio든 HTTP든, 메시지의 모양은 동일합니다 — 통로만 다를 뿐입니다.
flowchart LR
R["요청 Request\n(id 있음)"] -->|"답장 필요"| RESP["응답 Response\n(같은 id)"]
N["알림 Notification\n(id 없음)"] -.->|"답장 없음"| X["(끝)"]
classDef req fill:#fde68a,stroke:#b45309,color:#000;
classDef resp fill:#86efac,stroke:#15803d,color:#000;
classDef notif fill:#bfdbfe,stroke:#1d4ed8,color:#000;
class R,N req;
class RESP resp;
class X notif;
요청 (Request)
요청은 "어떤 메서드를, 어떤 파라미터로 실행해 달라"는 메시지입니다. 답을 받아야 하므로 id가 붙습니다.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "city": "Seoul" }
}
}
필드를 하나씩 보면:
| 필드 | 의미 |
|---|---|
jsonrpc | 항상 "2.0". 프로토콜 버전 표시 |
id | 이 요청의 추적 번호. 응답이 같은 id로 돌아온다 |
method | 실행할 메서드 이름 (예: tools/call, tools/list, resources/read) |
params | 메서드에 넘길 인자 (객체) |
id는 세션 안에서만 유일하면 됩니다. 각 요청·응답은 라우팅을 위해 유일한 id를 갖는데, 이 id는 세션별로 유일하므로 다른 세션에서는 같은 id(예: 4)를 충돌 없이 재사용할 수 있습니다. 보통 클라이언트가 1, 2, 3… 식으로 증가시킵니다.응답 (Response)
요청에 대한 답입니다. 성공이면 result, 실패면 error를 담습니다. 둘 중 하나만 옵니다.
성공:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{ "type": "text", "text": "Seoul: 23°C, 맑음" }
]
}
}
실패:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "Invalid params: city must be a string",
"data": { "path": "city", "expected": "string" }
}
}
응답의 id는 요청의 id와 같습니다. 그래서 클라이언트는 여러 요청을 동시에 보내 두고, 돌아오는 id로 "어느 요청의 답인지" 짝지을 수 있습니다.
알림 (Notification)
답장이 필요 없는 한 방향 메시지입니다. id가 없다는 점이 요청과의 결정적 차이입니다. MCP에서 알림은 생명주기 신호(notifications/initialized)나 동적 변경 통지(notifications/tools/list_changed)에 쓰입니다.
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
알림을 받은 쪽은 처리하되 응답을 보내지 않습니다. 예를 들어 세션 초기화 시 클라이언트가 핸드셰이크 완료 알림을 보내면, (HTTP 전송에서는) 서버가 본문 없이 202 Accepted로만 응답합니다.
id를 넣거나, 요청에 id를 빼먹는 것. id 유무가 "답장이 필요한가"를 결정합니다. 또 하나 — 응답에 result와 error를 둘 다 넣으면 안 됩니다. 정확히 하나만.표준 에러 코드
MCP는 JSON-RPC 2.0의 에러 표현을 그대로 씁니다. 무언가 잘못되면 code(숫자 식별자), message(사람이 읽을 설명), 선택적 data(추가 진단 정보)로 이루어진 에러 객체를 받습니다. JSON-RPC가 예약한 범위(-32768 ~ -32000)의 표준 코드는 다음과 같습니다.
| 코드 | 이름 | 언제 |
|---|---|---|
| -32700 | Parse error | JSON 문법 자체가 깨짐 |
| -32600 | Invalid Request | JSON-RPC 구조가 잘못됨 |
| -32601 | Method not found | 서버가 그 메서드를 모름 (버전 불일치·오타) |
| -32602 | Invalid params | 메서드는 있으나 인자가 요건에 안 맞음 (버전 협상 실패도 이 코드) |
| -32603 | Internal error | 서버 내부 처리 중 오류 |
예약 범위 밖의 코드는 애플리케이션 정의 에러로 자유롭게 쓸 수 있습니다. 많은 구현이 범주별로 코드를 정리하는 관례를 따릅니다(예: 인증 -31xxx, 리소스 접근 -30xxx). MCP가 별도로 정의한 코드도 있습니다(예: 리소스를 못 찾으면 -32002).
isError 플래그로 표현하는 패턴을 씁니다. 이 구분은 10장(Tools)과 16장(에러 처리)에서 자세히 다룹니다. 지금은 "메시지가 잘못된 것"과 "일이 실패한 것"은 다른 층위라는 점만 기억하세요.메서드 이름 규칙 한눈에
MCP 메서드는 명사/동사 또는 네임스페이스/동작 꼴을 따릅니다. 자주 보게 될 것들:
| 메서드 | 방향 | 하는 일 |
|---|---|---|
initialize | C → S | 핸드셰이크 시작 |
notifications/initialized | C → S (알림) | 핸드셰이크 완료 신호 |
tools/list | C → S | 도구 목록 요청 |
tools/call | C → S | 도구 실행 |
resources/list | C → S | 리소스 목록 |
resources/read | C → S | 리소스 읽기 |
prompts/list | C → S | 프롬프트 목록 |
prompts/get | C → S | 프롬프트 가져오기 |
notifications/tools/list_changed | S → C (알림) | 도구 목록 바뀜 통지 |
ping | 양방향 | 연결 살아있는지 확인 |
(전체 목록은 부록 A 치트시트에 정리합니다.)
🔒 보안 미리보기: 서버는params로 들어오는 모든 값을 신뢰할 수 없는 입력으로 다뤄야 합니다.arguments안의 문자열이 경로 조작(../../etc/passwd)이나 인젝션을 노릴 수 있습니다. 스키마 검증과 경계 검사는 서버의 책임입니다. (15장, 21장)
이 장에서 배운 것
- MCP의 모든 대화는 JSON-RPC 2.0(UTF-8)으로 인코딩되며, 전송 방식과 무관하게 메시지 모양은 같다.
- 세 종류: 요청(
id있음, 답장 필요), 응답(같은id,result또는error중 하나), 알림(id없음, 답장 없음). - 표준 에러 코드(-32700~-32603)는 프로토콜 실패용. 도구 실행 실패는 별도
isError패턴으로 표현한다. - 메서드는
tools/call,resources/read처럼 네임스페이스를 가진다.
✍️ 확인 문제
- 다음 메시지는 요청·응답·알림 중 무엇인가?
{"jsonrpc":"2.0","method":"notifications/initialized"}— 근거는? - 클라이언트가
id:5로tools/call을 보냈는데id:5에error: {code:-32601}이 돌아왔다. 무슨 뜻이고, 코드의 원인 후보는? - "도구가 호출한 외부 API가 500을 반환했다"는 상황은 JSON-RPC
error객체로 표현해야 할까, 아니면 다른 방식일까?
다음 장: 05. 초기화 핸드셰이크와 capability 협상