부록 A. JSON-RPC 2.0 메시지 포맷 레퍼런스
부록 A. JSON-RPC 2.0 메시지 포맷 레퍼런스
MCP를 디버깅하다 보면 결국 JSON-RPC 메시지를 직접 읽게 된다. 도구가 왜 호출되지 않는지, 응답이 왜 무시되는지, 클라이언트와 서버가 왜 서로 딴소리를 하는지의 답은 대개 메시지 안에 들어 있다.
이 부록은 JSON-RPC 2.0의 핵심 형식을 빠르게 찾아볼 수 있도록 정리한 레퍼런스다. 표준 전체를 길게 설명하기보다, MCP 서버를 만들고 운영할 때 실제로 자주 보는 뼈대에 집중하겠다.

JSON-RPC 2.0이란 무엇인가
JSON-RPC 2.0은 아주 단순한 원격 호출 규약이다. JSON으로 메시지를 만들고, 그 메시지 안에 "어떤 메서드를 어떤 인자로 호출할지", 그리고 "그 결과가 무엇인지"를 담는다.
핵심은 복잡한 전송 방식이 아니라 메시지의 모양이다. 다시 말해 stdio로 보내든, HTTP로 보내든, 웹소켓 비슷한 통로를 쓰든, 안에 들어 있는 요청·응답의 뼈대는 같다는 뜻이다. MCP도 바로 이 뼈대 위에 서 있다.
JSON-RPC 2.0에서 가장 먼저 눈에 들어와야 하는 키는 jsonrpc다. 값은 항상 문자열 "2.0"이다. 이 한 줄이 "나는 JSON-RPC 2.0 형식으로 말하고 있다"는 선언이다.
가장 먼저 보는 공통 필드
아래 표만 기억해도 메시지를 읽는 속도가 훨씬 빨라진다.
| 필드 | 위치 | 의미 |
|---|---|---|
jsonrpc | 모든 메시지 | 항상 "2.0" |
id | 요청, 응답 | 요청과 응답을 짝짓는 식별자 |
method | 요청, 알림 | 호출할 작업 이름 |
params | 요청, 알림 | 메서드에 넘길 인자 |
result | 성공 응답 | 호출이 성공했을 때의 결과 |
error | 실패 응답 | 호출이 실패했을 때의 오류 정보 |
여기서 가장 중요한 규칙은 하나다. 성공 응답에는 result가 있고, 실패 응답에는 error가 있다. 둘 다 동시에 있으면 안 된다.
요청(Request)
요청은 "이 메서드를 실행해 주세요"라고 말하는 메시지다. 응답을 기대하는 호출이므로 id가 반드시 들어간다.
가장 기본적인 형태는 이렇다.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}이 메시지를 읽는 순서는 단순하다. JSON-RPC 2.0 형식이고, 요청 번호는 1이며, tools/list 메서드를 호출하고, 인자는 빈 객체라는 뜻이다.
요청 필드 규칙
| 필드 | 필수 여부 | 설명 |
|---|---|---|
jsonrpc | 필수 | 항상 "2.0" |
id | 필수 | 문자열, 숫자, 또는 null이 될 수 있지만 MCP에서는 보통 문자열이나 숫자를 쓴다 |
method | 필수 | 호출 대상 메서드 이름 |
params | 선택 | 객체 또는 배열이 가능하지만 MCP에서는 보통 객체를 쓴다 |
실전에서는 params를 거의 항상 객체로 본다고 생각해도 된다. 이름 있는 인자를 읽고 디버깅하기가 훨씬 쉽기 때문이다.
예시: 도구 호출 요청
PKM-MCP의 search_notes 도구를 호출하는 장면을 JSON-RPC로 보면 대략 이런 느낌이다.
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "search_notes",
"arguments": {
"query": "MCP 인증 흐름",
"limit": 5
}
}
}겉보기에는 평범한 JSON이지만, 디버깅 포인트는 많다. method가 맞는지, 도구 이름이 맞는지, arguments 구조가 서버가 기대한 모양과 같은지, limit 타입이 숫자인지 하나씩 확인하면 된다.
성공 응답(Response: success)
성공 응답은 요청에 대한 정상적인 결과를 돌려준다. 이때 응답의 id는 반드시 원래 요청의 id와 같아야 한다.
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"matches": [
{
"id": "note-101",
"title": "OAuth 메모",
"snippet": "MCP에서 OAuth를 붙일 때는..."
}
]
}
}이 메시지를 읽을 때는 먼저 id를 본다. 요청 42번에 대한 응답인지 확인하는 것이다. 그다음 result의 구조를 보면 된다. 만약 result의 구조가 클라이언트가 기대한 모양과 다르면, 호출 자체는 성공했더라도 화면에는 아무것도 안 보일 수 있다.
성공 응답 규칙
| 필드 | 필수 여부 | 설명 |
|---|---|---|
jsonrpc | 필수 | 항상 "2.0" |
id | 필수 | 요청의 id와 동일해야 함 |
result | 필수 | 성공 결과 |
error | 금지 | 성공 응답에는 있으면 안 됨 |
실패 응답(Response: error)
실패 응답은 호출이 처리되지 못했을 때 보낸다. 성공 응답과 마찬가지로 id는 원래 요청과 같아야 한다.
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"field": "limit",
"reason": "must be >= 1"
}
}
}여기서 중요한 것은 message만 읽고 끝내지 않는 습관이다. 진짜 디버깅 정보는 종종 data 안에 숨어 있다. 어떤 필드가 잘못되었는지, 왜 실패했는지, 다시 시도할 수 있는지 같은 힌트가 그 안에 들어간다.
실패 응답 규칙
| 필드 | 필수 여부 | 설명 |
|---|---|---|
jsonrpc | 필수 | 항상 "2.0" |
id | 원칙적으로 포함 | 어떤 요청에 대한 실패인지 식별 |
error.code | 필수 | 오류 코드 |
error.message | 필수 | 사람이 읽을 수 있는 짧은 설명 |
error.data | 선택 | 추가 디버깅 정보 |
result | 금지 | 실패 응답에는 있으면 안 됨 |
알림(Notification)
알림은 응답을 기대하지 않는 메시지다. 그래서 id가 없다. 이 차이가 아주 중요하다. id가 없으면 상대는 답장을 보내지 않는다.
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progress": 50,
"message": "인덱스를 재구성하는 중입니다"
}
}알림은 "이 사실만 알려둘게요"에 가깝다. 진행 상황 통지, 상태 변경 알림, 구독 기반 이벤트 전달 같은 곳에서 자주 등장한다.
알림 규칙
| 필드 | 필수 여부 | 설명 |
|---|---|---|
jsonrpc | 필수 | 항상 "2.0" |
method | 필수 | 알릴 메서드 이름 |
params | 선택 | 알림에 실을 데이터 |
id | 금지 | 알림에는 없어야 함 |
초보자가 가장 많이 하는 실수 중 하나는 알림에 id를 넣어 놓고 왜 응답이 안 오느냐고 묻는 것이다. 반대로 요청인데 id를 빼먹으면, 서버는 그 메시지를 알림처럼 받아들이고 응답하지 않을 수 있다.
id는 왜 이렇게 중요한가
JSON-RPC에서 id는 우체국의 등기번호 같은 역할을 한다. 요청과 응답을 연결해 주는 고리다. 동시에 여러 요청이 오갈 수 있기 때문에, id가 없거나 뒤섞이면 누가 누구의 답장인지 알 수 없게 된다.
실무에서는 다음 원칙을 지키면 안전하다.
요청마다 고유한
id를 쓴다.응답은 반드시 같은
id를 돌려준다.알림에는
id를 넣지 않는다.
문자열 id를 쓰는 팀도 많고, 숫자 id를 쓰는 팀도 많다. 무엇을 쓰든 일관성이 더 중요하다.
params는 객체로 생각하자
JSON-RPC 2.0 자체는 params에 배열도 허용한다. 하지만 MCP 문맥에서는 객체를 쓰는 편이 훨씬 낫다.
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "add_note",
"arguments": {
"title": "오늘의 메모",
"content": "JSON-RPC는 생각보다 단순하다"
}
}
}배열 기반 인자는 순서를 틀리면 읽기도 어렵고, 오류 메시지도 불친절해지기 쉽다. 반면 객체 기반 인자는 필드 이름이 그대로 문서 역할을 한다. 디버깅할 때도 훨씬 직관적이다.
JSON-RPC 표준 오류 코드 빠른 참조
디버깅에서 자주 만나는 대표 코드는 아래와 같다.
| 코드 | 이름 | 뜻 |
|---|---|---|
-32700 | Parse error | JSON 자체가 깨졌거나 파싱할 수 없음 |
-32600 | Invalid Request | JSON은 읽혔지만 JSON-RPC 요청 형식이 아님 |
-32601 | Method not found | 해당 메서드를 지원하지 않음 |
-32602 | Invalid params | 인자 형식이나 값이 잘못됨 |
-32603 | Internal error | 서버 내부 처리 중 일반적인 오류 발생 |
이 표를 외워 두면 오류 메시지를 보는 눈이 빨라진다. 예를 들어 -32601이 보이면 인증이나 데이터베이스를 의심하기 전에 먼저 메서드 이름 철자를 확인하는 편이 맞다.
MCP에서 자주 보는 메시지 읽는 법
JSON-RPC를 추상적으로만 알면 막상 로그를 볼 때 손이 멈춘다. MCP에서 실제로 자주 마주치는 형태를 짧게 읽어 보자.
초기화 요청
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {
"name": "Example Client",
"version": "1.0.0"
}
}
}이 메시지는 "우리 대화를 시작하자"에 해당한다. 프로토콜 버전이 맞는지, 클라이언트가 어떤 기능을 지원하는지 협상하는 입구다. 연결은 되었는데 아무 일도 안 일어나면, 종종 이 단계에서 이미 어긋나 있다.
도구 목록 요청
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}이 메시지는 "당신이 제공하는 도구를 보여 주세요"라는 뜻이다. 서버가 정상인데 클라이언트에 도구가 안 뜬다면, 이 요청에 대한 응답 구조가 기대와 다를 가능성이 높다.
도구 호출 요청
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "search_notes",
"arguments": {
"query": "RAG",
"limit": 3
}
}
}이 장면에서는 name과 arguments를 먼저 본다. 그다음 실제 에러가 나면 서버 로그에서 검증 로직과 타입 변환 부분을 함께 확인하면 된다.
한눈에 보는 요청·응답·알림 비교
| 구분 | id | method | params | result | error |
|---|---|---|---|---|---|
| 요청 | 있음 | 있음 | 선택 | 없음 | 없음 |
| 성공 응답 | 있음 | 없음 | 없음 | 있음 | 없음 |
| 실패 응답 | 있음 | 없음 | 없음 | 없음 | 있음 |
| 알림 | 없음 | 있음 | 선택 | 없음 | 없음 |
이 표는 디버깅할 때 특히 유용하다. 메시지 하나를 봤는데 id도 있고 method도 있고 result도 있다면, 그건 거의 틀린 메시지다. 형식이 뒤섞였다는 뜻이다.
가장 흔한 실수들
jsonrpc를 빼먹는다
가끔 프레임워크가 알아서 넣어 줄 것이라 생각하고 jsonrpc를 생략하는 경우가 있다. 하지만 JSON-RPC 2.0 메시지라면 이 필드는 기본 중의 기본이다.
성공인데 error도 같이 넣는다
사람 마음은 늘 불안해서, 성공 결과를 보내면서 참고용 오류 정보도 같이 넣고 싶어진다. 하지만 JSON-RPC는 그렇게 설계되지 않았다. 성공이면 result, 실패면 error다. 둘 중 하나만 선택해야 한다.
요청인데 id가 없다
이 경우 상대는 응답할 의무가 없다. 실제로는 메시지가 잘 전달되었는데도, 보낸 쪽에서는 "서버가 죽었다"고 오해하기 쉽다.
응답의 id가 원래 요청과 다르다
동시 요청이 많아질수록 치명적이다. 응답 자체는 잘 만들어도, id가 다르면 클라이언트는 그 응답을 버리거나 엉뚱한 요청과 연결할 수 있다.
params 구조가 문서와 다르다
메서드 이름은 맞는데 동작하지 않을 때 가장 먼저 의심할 지점이다. 특히 arguments 안에 들어가야 할 값을 params 바로 아래에 넣는 실수가 잦다.
로그에서 메시지를 읽는 순서
메시지를 한 줄씩 뜯어보다 보면 금세 지친다. 순서를 정해 놓으면 훨씬 빨라진다.
첫째, jsonrpc가 "2.0"인지 본다. 둘째, 이 메시지가 요청인지 응답인지 알림인지 구분한다. 셋째, id가 필요한 자리에 있는지 확인한다. 넷째, method 이름과 params 구조를 본다. 다섯째, 실패라면 error.code와 error.data를 함께 읽는다.
이 다섯 단계만 습관이 되어도, 디버깅 시간은 눈에 띄게 줄어든다.
사람이 읽기 좋은 오류를 만드는 법
JSON-RPC는 기계끼리 대화하는 규약이지만, 오류 메시지는 결국 사람이 읽는다. 그래서 error.message는 짧고 분명해야 하고, error.data에는 실제 해결에 도움이 되는 정보를 담아야 한다.
나쁜 오류는 이런 식이다.
{
"jsonrpc": "2.0",
"id": 99,
"error": {
"code": -32603,
"message": "Something went wrong"
}
}이 메시지는 틀리지는 않았지만, 디버깅에는 거의 도움이 되지 않는다.
조금 더 나은 오류는 이렇게 생긴다.
{
"jsonrpc": "2.0",
"id": 99,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"field": "query",
"reason": "query must not be empty"
}
}
}이제는 무엇을 고쳐야 하는지가 보인다. 클라이언트 개발자도, 서버 개발자도, 심지어 로그를 뒤지는 미래의 나도 훨씬 편해진다.
배치(Batch)는 알아만 두자
JSON-RPC 2.0은 여러 요청을 배열로 한 번에 보내는 배치도 허용한다. 예를 들면 이런 식이다.
[
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
},
{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/list"
}
]다만 MCP를 처음 다룰 때는 배치보다 단일 메시지 흐름을 확실히 이해하는 편이 훨씬 중요하다. 디버깅도 단일 요청 기준으로 먼저 익히는 것이 좋다.
실전 디버깅 체크리스트
로그를 보며 아래 항목을 빠르게 점검해 보자.
메시지에
jsonrpc: "2.0"가 있는가요청이라면
id가 있는가알림이라면
id가 없는가응답의
id가 원래 요청과 같은가성공 응답에
result만 있고error는 없는가실패 응답에
error만 있고result는 없는가method이름이 정확한가params구조가 서버 문서와 같은가오류가 나면
error.data까지 확인했는가
체크리스트는 사소해 보여도 강력하다. 대부분의 문제는 이 범위 안에서 잡힌다.
PKM-MCP를 디버깅할 때 특히 볼 것
메인 프로젝트인 PKM-MCP에서는 search_notes, semantic_search_notes, add_note, update_note, delete_note 같은 도구가 점점 늘어난다. 도구가 많아질수록 코드보다 메시지 레벨에서 문제를 좁히는 습관이 중요해진다.
예를 들어 키워드 검색은 잘 되는데 의미 검색만 실패한다면, 벡터 DB를 의심하기 전에 먼저 tools/call 요청의 name이 정확한지, arguments 안의 필드명이 서버 스펙과 같은지, 응답이 result 대신 error로 끝났는지 확인하자. OAuth를 붙인 뒤부터 실패한다면 인증 미들웨어만 보지 말고, 요청 자체가 초기화 이후 정상 순서로 오가는지도 함께 봐야 한다.
결국 MCP 디버깅의 출발점은 같다. "서버가 무슨 생각을 했는가?"가 아니라 "실제로 어떤 메시지가 오갔는가?"를 보는 것이다.
최소 암기본
마지막으로, 정말 바쁠 때는 이것만 기억해도 된다.
요청은 jsonrpc, id, method를 가진다. 성공 응답은 jsonrpc, id, result를 가진다. 실패 응답은 jsonrpc, id, error를 가진다. 알림은 jsonrpc, method를 가지지만 id는 없다.
이 네 문장만 머릿속에 들어 있으면, 낯선 MCP 로그를 봐도 길을 잃지 않는다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.