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의 모든 대화를 읽을 수 있습니다.

💡 팁
REST API와 어떻게 다른가? 웹에서 흔한 REST는 "자원(URL)에 대해 GET/POST 같은 HTTP 동작을 한다"는 관점입니다. JSON-RPC는 "이름이 붙은 메서드(함수)를 호출한다"는 관점이라 더 함수 호출에 가깝습니다. MCP가 RPC 방식을 고른 건, 결국 "도구를 호출하고 결과를 받는" 일이 원격 함수 호출과 잘 맞기 때문입니다.

우편물 비유

이제 비유로 직관을 잡아 봅시다. 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가 붙습니다.

JSON
{
  "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를 담습니다. 둘 중 하나만 옵니다.

성공:

JSON
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      { "type": "text", "text": "Seoul: 23°C, 맑음" }
    ]
  }
}

실패:

JSON
{
  "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)에 쓰입니다.

JSON
{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

알림을 받은 쪽은 처리하되 응답을 보내지 않습니다. 예를 들어 세션 초기화 시 클라이언트가 핸드셰이크 완료 알림을 보내면, (HTTP 전송에서는) 서버가 본문 없이 202 Accepted로만 응답합니다.

⚠️ 흔한 실수
흔한 실수: 알림에 id를 넣거나, 요청에 id를 빼먹는 것. id 유무가 "답장이 필요한가"를 결정합니다. 또 하나 — 응답에 resulterror둘 다 넣으면 안 됩니다. 정확히 하나만.

표준 에러 코드

MCP는 JSON-RPC 2.0의 에러 표현을 그대로 씁니다. 무언가 잘못되면 code(숫자 식별자), message(사람이 읽을 설명), 선택적 data(추가 진단 정보)로 이루어진 에러 객체를 받습니다. JSON-RPC가 예약한 범위(-32768 ~ -32000)의 표준 코드는 다음과 같습니다.

코드이름언제
-32700Parse errorJSON 문법 자체가 깨짐
-32600Invalid RequestJSON-RPC 구조가 잘못됨
-32601Method not found서버가 그 메서드를 모름 (버전 불일치·오타)
-32602Invalid params메서드는 있으나 인자가 요건에 안 맞음 (버전 협상 실패도 이 코드)
-32603Internal error서버 내부 처리 중 오류

예약 범위 밖의 코드는 애플리케이션 정의 에러로 자유롭게 쓸 수 있습니다. 많은 구현이 범주별로 코드를 정리하는 관례를 따릅니다(예: 인증 -31xxx, 리소스 접근 -30xxx). MCP가 별도로 정의한 코드도 있습니다(예: 리소스를 못 찾으면 -32002).

📌 핵심
핵심 구분 — 프로토콜 에러 vs 도구 실행 에러: 위 에러 객체는 프로토콜 수준의 실패(잘못된 메시지, 없는 메서드 등)에 씁니다. 반면 도구가 실행은 됐는데 결과가 실패인 경우(예: API가 404 반환)는 JSON-RPC 에러가 아니라, 도구 결과 안의 isError 플래그로 표현하는 패턴을 씁니다. 이 구분은 10장(Tools)16장(에러 처리)에서 자세히 다룹니다. 지금은 "메시지가 잘못된 것"과 "일이 실패한 것"은 다른 층위라는 점만 기억하세요.

메서드 이름 규칙 한눈에

MCP 메서드는 명사/동사 또는 네임스페이스/동작 꼴을 따릅니다. 자주 보게 될 것들:

메서드방향하는 일
initializeC → S핸드셰이크 시작
notifications/initializedC → S (알림)핸드셰이크 완료 신호
tools/listC → S도구 목록 요청
tools/callC → S도구 실행
resources/listC → S리소스 목록
resources/readC → S리소스 읽기
prompts/listC → S프롬프트 목록
prompts/getC → S프롬프트 가져오기
notifications/tools/list_changedS → 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처럼 네임스페이스를 가진다.

✍️ 확인 문제

  1. 다음 메시지는 요청·응답·알림 중 무엇인가? {"jsonrpc":"2.0","method":"notifications/initialized"} — 근거는?
  2. 클라이언트가 id:5tools/call을 보냈는데 id:5error: {code:-32601}이 돌아왔다. 무슨 뜻이고, 코드의 원인 후보는?
  3. "도구가 호출한 외부 API가 500을 반환했다"는 상황은 JSON-RPC error 객체로 표현해야 할까, 아니면 다른 방식일까?
다음 장: 05. 초기화 핸드셰이크와 capability 협상