-
JSON-RPC 2.0이 정의하는 건 봉투 6단어뿐: MCP 사례로 그 안과 밖을 가른다IT 2026. 6. 12. 21:00
MCP(Model Context Protocol, AI 모델과 외부 도구·데이터를 잇는 공개 표준 — 흔히 "AI용 USB-C 포트"라 불린다) 서버를 직접 개발하려고 명세를 펼쳐보면, 와이어 포맷이 두 종류의 약속이 포개진 모양을 하고 있다. 가장 바깥은 JSON-RPC 2.0이라는 호출 규약, 그 안은 MCP가 메서드별로 정한 구조다.
inputSchema·content·tools/call같은 이름이 어느 쪽 약속에 속하는지부터 헷갈리는 게 보통이다. 결론부터 적으면 JSON-RPC는 봉투 6단어만 정의하고, 그 안의 내용물은 전부 MCP가 채운다. MCP를 다루기 전에 그 봉투부터 먼저 짚고 넘어가자 — 이 글은 JSON-RPC 2.0의 봉투 6단어를 MCP 도구 호출 사례로 풀어본다.배경: JSON-RPC 2.0이 정의하는 것의 전부
JSON-RPC 2.0은 원격 함수 호출(Remote Procedure Call, 다른 프로세스의 함수를 마치 로컬 함수처럼 호출하는 패턴) 규약 중 가장 가벼운 축에 든다. HTTP REST가 URL·메서드(GET/POST)·헤더로 의미를 표현하는 반면, JSON-RPC는 그런 거 다 빼고 모든 호출을 단일 JSON 객체로 통일한다. 전송 계층(transport)도 정해두지 않는다 — HTTP·stdio(표준 입출력)·WebSocket 무엇이든 좋다. MCP는 주로 stdio를 쓴다.
그래서 JSON-RPC가 정의하는 건 메시지 두 가지 모양뿐이다 — 요청(Request)과 응답(Response). 각각 필드 네다섯 개로 끝난다.
요청(Request) — 4필드
{ "jsonrpc": "2.0", // 규약 버전 — 항상 "2.0" 문자열 "id": 7, // 이 호출의 고유 번호 (응답에서 그대로 돌아온다) "method": "tools/call", // 호출할 메서드 이름 "params": { ... } // 메서드 인자 (객체 또는 배열, 선택) }코드 설명: 네 필드 중
jsonrpc·method는 필수,id는 응답을 짝지을 때 필수(생략하면 "알림"이 된다 — 뒤에서 다룬다),params는 메서드가 인자를 받을 때만 둔다.method값이tools/call인지resources/read인지 JSON-RPC는 모른다 — 그건 윗 계층(MCP)이 정한 이름이고, JSON-RPC 입장에서는 그냥 문자열이다.응답(Response) — 성공이면 result, 실패면 error
// 성공 { "jsonrpc": "2.0", "id": 7, // 요청과 같은 번호 "result": { ... } // 성공 시의 반환값 (임의 JSON) } // 실패 { "jsonrpc": "2.0", "id": 7, "error": { "code": -32602, // 표준 에러 코드 "message": "Invalid params", "data": { ... } // 부가 정보 (선택) } }코드 설명: 성공이면
result가, 실패면error가 들어온다 — 둘은 동시에 존재할 수 없다.id는 요청 때 보낸 값을 그대로 돌려주므로 클라이언트는 응답이 어느 호출에 대응되는지 안다(stdio처럼 응답 순서가 보장되지 않는 전송에서 특히 중요).error.code의 -32700~-32603은 JSON-RPC가 예약한 표준값이고, -32099~-32000은 구현이 자유롭게 정의하는 범위다.왜 중요한가: JSON-RPC 규격으로 외울 건 이게 전부다.
jsonrpc·id·method·params·result·error— 봉투 6단어. 그 안의 내용물이 어떤 모양이어야 하는지는 JSON-RPC가 일절 말하지 않는다.사례 1: tools/list — "어떤 도구가 있나요?"
LLM(코딩 어시스턴트)이 MCP 서버에 "지금 쓸 수 있는 도구가 뭐가 있나요?"를 묻는, 가장 흔한 호출부터 보자. 메서드 이름은
tools/list다.다이어그램 설명: 클라이언트는
id:1을 붙여 보내고, 서버는 같은id:1을 응답에 박아 돌려준다. JSON-RPC가 보장하는 건 이 봉투의 짝맞춤(id)까지다 —result안의tools배열 모양은 JSON-RPC 명세에 없는 MCP의 별도 약속이다.// 실제 와이어 (요청) { "jsonrpc": "2.0", "id": 1, "method": "tools/list" } // 실제 와이어 (응답) { "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "build-app", "description": "프로젝트를 빌드한다", "inputSchema": { "type": "object", "properties": { "project_dir": { "type": "string" }, "configuration": { "type": "string", "enum": ["Debug", "Release"] }, "arch": { "type": "string", "enum": ["arm64", "x86_64"] } }, "required": ["project_dir", "arch"] } }, { "name": "lint-check", "description": "코드 린트 검사를 실행한다", "inputSchema": { "type": "object", "properties": {} } } ] } }코드 설명: 봉투(
jsonrpc·id·result)는 JSON-RPC가 정한 모양이다. 그러나result.tools[].name·description·inputSchema는 JSON-RPC와 무관하다 — MCP 명세가 "tools/list의 응답은 이런 모양이어야 한다"고 별도로 못 박은 약속이다. 흥미로운 점은 인자 명세(inputSchema, JSON Schema 형식)까지 이 한 번의 응답에 통째로 실려 온다는 것이다 — LLM 클라이언트는tools/list한 라운드트립으로 "어떤 도구가 있나"와 "어떤 인자로 불러야 하나"를 동시에 안다. 별도 describe 호출 단계가 없다.사례 2: tools/call — "이 도구를 이 인자로 실행해줘"
실제 도구를 실행하는 호출이다.
method가tools/call로 고정되고, 어떤 도구를 어떤 인자로 부르는지는params안에 한 번 더 객체로 들어간다.// 요청 { "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "build-app", "arguments": { "project_dir": "./app", "configuration": "Release", "arch": "arm64" } } } // 응답 { "jsonrpc": "2.0", "id": 7, "result": { "structuredContent": { "exit_code": 0, "output": "app-release-arm64.pkg" }, "isError": false } }코드 설명: 요청에서
method는 항상tools/call이고, 실제 호출할 도구 이름은params.name에 들어간다. 즉 JSON-RPC 봉투의method는 "지금 너에게 시킬 동작 카테고리"이고, 실제 도구 이름은 MCP가 한 단계 더 안쪽에서 다룬다 — JSON-RPC 메서드 이름이 도구마다 늘어나지 않도록 한 설계다. 응답result안도 MCP 약속이다 —structuredContent라는 객체 필드에 우리 도구의 JSON이 구조 그대로 실린다. 문자열로 직렬화하지 않으니 클라이언트는 곧바로result.structuredContent.exit_code처럼 점 접근으로 값을 꺼낸다.왜 중요한가: 봉투 안의 봉투 구조다. 가장 바깥은 JSON-RPC가 정한
result, 그 안은 MCP가 정한structuredContent, 다시 그 안은 우리가 정한{ exit_code, output }. 세 층이 각자 한 가지 결정만 책임진다 — 그리고 그 세 층이 다 같은 JSON 트리 안에 자연스럽게 중첩된다.핵심 질문: 어디까지가 JSON-RPC이고 어디부터가 MCP인가
그림: 세 계층은 각자 다른 표준이 책임진다 다이어그램 설명: 가장 바깥 회색이 JSON-RPC 2.0이 정의하는 6단어다. 그 안 파란 영역이 MCP가 추가한 약속 — 메서드 이름의 값(
tools/list·tools/call등),params안에 다시 들어가는name·arguments구조,result안에 들어가는structuredContent·tools·inputSchema구조. 가장 안쪽 초록은 MCP 서버를 만드는 사람이 자유롭게 정하는 도구별 출력(exit_code·output·job_id등 — 위 예시는 한 가지 설계안)이고,structuredContent객체에 그대로 실려 들어간다. 세 층은 각자 한 가지 결정만 책임진다 — 봉투(JSON-RPC) / 도메인 계약(MCP) / 도구별 페이로드(서버 구현자).왜 중요한가: 같은 JSON-RPC 봉투 위에 MCP·Ethereum JSON-RPC API·Language Server Protocol(LSP)이 전부 얹힐 수 있다 — 봉투는 공유하고 메서드 이름·파라미터·결과 구조만 도메인별로 갈린다. 봉투를 일부러 좁게 둔 덕에 윗 계층이 자유롭게 진화한다.
사례 3: 에러 응답 — params가 잘못됐을 때
LLM이
arch에 스키마가 허용하지 않는 값("ARM")을 넣어 보냈다고 하자. 서버는 도구를 실행하기 전에inputSchema로 검증하고 에러를 돌려준다.// 요청 (arch 값이 enum에 없음) { "jsonrpc": "2.0", "id": 8, "method": "tools/call", "params": { "name": "build-app", "arguments": { "project_dir": "./app", "arch": "ARM" } } } // 응답 { "jsonrpc": "2.0", "id": 8, "error": { "code": -32602, "message": "Invalid params", "data": { "field": "arch", "expected": ["arm64", "x86_64"], "received": "ARM" } } }코드 설명: 응답에
result대신error가 온다.code: -32602는 JSON-RPC가 예약한 "Invalid params" 표준 코드 — 어떤 구현이든 이 숫자는 같은 의미다.message도 표준 문구가 권장된다. 진짜 디버깅 정보는data필드에 들어가는데, 이 안쪽 구조는 다시 구현 자유다(MCP가 약속을 더할 수도, 안 정해두면 서버 마음대로).JSON-RPC가 예약한 표준 에러 코드는 다섯 개로 짧다.
-32700 Parse error — JSON 자체가 깨졌다 -32600 Invalid Request — 봉투 모양이 틀렸다 (jsonrpc 필드 누락 등) -32601 Method not found — method 이름이 등록 안 됨 -32602 Invalid params — params가 메서드가 요구하는 모양 아님 -32603 Internal error — 서버 내부 버그 -32099 ~ -32000 — 구현이 자유롭게 쓰는 Server error 범위왜 중요한가: 에러 코드가 표준이라는 건 LLM 입장에서 분기 비용이 0이라는 뜻이다. -32602가 오면 "내가 인자를 잘못 보냈군"이라는 결론이 즉시 나온다 — 자연어 메시지를 정규식으로 파싱할 필요가 없다. JSON-RPC가 자연어 대신 숫자 코드를 못 박아둔 동기가 여기 있다.
사례 4: 알림(Notification) — 응답을 기대하지 않는 호출
요청에서
id를 빼면 그 메시지는 "알림(notification)"이 된다. 서버는 응답을 돌려주지 않는다. MCP에서는 초기화 완료 같은 일방향 신호에 주로 쓴다.다이어그램 설명: 위 두 줄은 일반 호출 —
id로 요청·응답이 짝지어진다. 아래 한 줄은 알림 —id가 없다는 점 하나로 메시지 의미가 바뀌고, 서버는 화살표를 되돌려 보내지 않는다. JSON-RPC는id유무 하나로 두 패턴을 구분한다.// 클라이언트가 서버에게 "나 준비 끝" 알리는 알림 { "jsonrpc": "2.0", "method": "notifications/initialized" } // id가 없다 → 서버는 응답 안 함코드 설명: 서버가 알림 처리에 실패해도 클라이언트는 알 길이 없다 — 알림은 "받았으면 좋고 안 받았어도 안 망가지는" 종류의 신호에 한정해 써야 한다. MCP는 초기화 완료·progress 갱신·cancel 요청 같은 곳에서 이 패턴을 쓴다.
notifications/라는 prefix 자체도 MCP 컨벤션이지 JSON-RPC가 강제한 건 아니다 — JSON-RPC는 그냥 "method 문자열"로만 본다.핵심 통찰: 봉투를 일부러 좁게 둔 이유
JSON-RPC가 봉투 6단어로 끝나는 건 모자란 게 아니라 의도다. 봉투는 전송 무관·도메인 무관한 범용 짝맞춤만 책임지고(
id로 요청·응답 페어링,method로 디스패치,error로 실패 신호), 내용물은 도메인이 알아서 정한다.그래서 같은 JSON-RPC 위에 세 가지가 평화롭게 공존한다 — MCP(AI 도구 호출), Ethereum JSON-RPC API(블록체인 RPC), LSP(IDE-언어서버 통신). 봉투는 공유하고 메서드 이름·파라미터·결과 구조만 도메인별로 갈린다. 메서드 이름이
tools/call이든eth_getBalance든textDocument/completion이든, JSON-RPC 입장에서는 그냥 디스패치 키일 뿐이다.MCP 서버 개발자 관점에서 책임이 어떻게 나뉘는지 정리하면 이렇다.
JSON-RPC 2.0이 정한 것 (이 글의 범위): jsonrpc · id · method · params · result · error 표준 에러 코드 -32700 ~ -32603 id 유무로 알림 구분 MCP가 정한 것 (다음 글의 범위): method 값들: tools/list, tools/call, resources/read, notifications/initialized, ... params 구조: { name, arguments } result 구조 (tools/list): { tools: [{ name, description, inputSchema }] } result 구조 (tools/call): { structuredContent, isError } inputSchema가 JSON Schema라는 점 (tools/list 응답에 직접 실림) MCP 서버 개발자가 정하는 것: 도구별 페이로드 (예: { exit_code, output } / { job_id, status, ... } / { logs, next_offset, eof }) structuredContent에 객체 그대로 실려 들어감코드 설명:
inputSchema·structuredContent는 어디에 있는지 보자 — 둘 다 MCP가 정한 자리에 들어 있다.inputSchema는tools/list의result.tools[]안,structuredContent는tools/call의result안. JSON-RPC가 정의한 건 그 바깥의result라는 슬롯이 있다는 사실까지다 — 그 안에 무엇이 들어가야 하는지는 도메인 명세(MCP)가 메서드별로 별도로 못 박는다.왜 중요한가: JSON-RPC를 안다고 MCP를 다 아는 건 아니다. 반대도 마찬가지 — MCP만 외운 사람은 봉투 6단어가 어디서 왔는지 모른다. 둘은 따로 학습해야 하는 별개의 약속이고, 그게 분리되어 있다는 사실 자체가 두 표준이 각자 진화할 수 있는 이유다. MCP 명세가 다음 버전에서
structuredContent형식을 더 다듬어도 JSON-RPC 봉투는 한 글자도 안 바뀐다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
MCP Roots 완전 분해: 서버가 클라이언트에게 먼저 묻는 역방향 설계 (0) 2026.06.13 MCP Prompts의 멀티턴 messages — 서버가 모델의 첫 생각을 설계하는 방법 (0) 2026.06.13 MCP Prompts 완전 분해: 최적 프롬프트를 서버에 봉인하고 재사용하는 방법 (0) 2026.06.13 MCP Resources 완전 분해: URI로 AI에게 데이터를 공급하는 7가지 메서드 (0) 2026.06.12 MCP가 JSON-RPC 봉투 안에 채운 것들: 세 기본 단위와 20가지 메서드 전체 지도 (0) 2026.06.12 서브에이전트 패키지를 직접 뜯어보다 — debug-pack 플러그인 해부 (0) 2026.06.11 Claude에 브라우저 눈과 손을 달다 — Playwright MCP 플러그인 (0) 2026.06.10 Claude에 GitHub 전체를 연결하다 — GitHub MCP 플러그인 실전 가이드 (0) 2026.06.10 Claude Code에서 나만의 AI 전문가 만들기 — 서브에이전트 제작 가이드 (0) 2026.06.10 품질과 지식의 정합성 보장, OpenCode의 자율 검증(Verify) 메커니즘 (0) 2026.06.09