MCP 프로토콜 — JSON-RPC·stdio·Streamable HTTP
이 장에서 처음 나오는 말6개
JSON-RPC 2.0- method 이름과 params를 담은 JSON 요청에 같은 id의 결과나 오류가 돌아오는 원격 호출 규약이다. 전송 방식을 정하지 않아 MCP가 그 위에 얹혔다.
stdio transportStandard Input/Output Transport- client가 server를 자식 process로 띄우고 한 줄에 message 하나씩 표준 입출력으로 주고받는 방식이다. 포트가 없다.
Streamable HTTP- server가 HTTP endpoint 하나를 열고 message마다 POST를 받는 방식이다. 응답은 JSON 하나이거나 SSE stream이다.
SSEServer-Sent Events- HTTP 응답을 닫지 않고 event를 여러 개 흘려보내는 표준이다. MCP에서는 별도 전송이 아니라 응답의 한 형태다.
capability- 초기화 때 client와 server가 서로 "나는 이것을 지원한다"고 선언하는 항목이다. 선언하지 않은 기능은 쓰지 않는다.
host · client · server- host는 Claude Desktop·kagent engine 같은 응용이고, 그 안의 client가 server 하나와 1:1로 연결된다. server는 tool·resource·prompt를 제공한다.
이 덱은 지금까지 MCP server를 Tool 공급 계약의 단위로, kagent의
MCPServer·RemoteMCPServer resource로, 그리고 proxy가
중개하는 대상으로 다뤘다. 그 사이 “실제로 wire에 무엇이 오가는가”는 건너뛰었다. 이 장은 그 빈칸을
채운다. 읽고 나면 kagent가 tools/list로 발견한 이름이 toolNames allowlist와 어떻게 맞물리는지,
stdio server를 network에 내놓으려면 왜 sidecar가 필요한지, 8084 같은 포트가 프로토콜의 일부가
아닌 이유를 wire 수준에서 답할 수 있다.
세 층으로 읽는다
섹션 제목: “세 층으로 읽는다”MCP 스펙은 한 덩어리가 아니라 세 층이다. 아래에서 위로 읽으면 각 층이 무엇을 정하고 무엇을 정하지 않는지가 보인다.
포트·인증 방식·프레임워크는 어느 층에도 없다. 전송 층이 “HTTP endpoint 하나”까지만 정하고 그 endpoint가 어느 포트에 붙는지는 배포하는 쪽의 선택이다. 이 덱에서 보는 숫자들은 모두 제품의 기본값이다.
| 숫자 | 정체 |
|---|---|
| 3000 | kmcp MCPServer의 deployment.port 기본값 |
| 8084 | kagent가 기본 배포하는 내장 tool server kagent-tools의 service 포트 |
| 8000 | FastMCP(Python)가 HTTP를 열 때의 기본값 |
message 층 — JSON-RPC 2.0
섹션 제목: “message 층 — JSON-RPC 2.0”모든 message는 JSON-RPC 2.0이고 UTF-8이다. 세 종류뿐이다.
| 종류 | 생김새 | 규칙 |
|---|---|---|
| request | {"jsonrpc":"2.0","id":1,"method":"tools/list","params":{…}} | id는 문자열이나 정수. null 금지, 같은 session 안에서 재사용 금지 |
| response | {"jsonrpc":"2.0","id":1,"result":{…}} 또는 {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"…"}} | request와 같은 id. result와 error 중 하나만 |
| notification | {"jsonrpc":"2.0","method":"notifications/initialized"} | id가 없고 응답도 없다 |
두 가지가 MCP 고유다. params와 result 안의 _meta는 프로토콜이 예약한 자리라 progress token·trace
context 같은 부가 정보가 여기 실린다. 그리고 method 이름은 tools/list·resources/read처럼
<기능>/<동작> 꼴이고 알림은 notifications/로 시작한다. 이 규칙만 알면 처음 보는 method도 어느 층의
무엇인지 읽힌다.
전송 층 1 — stdio
섹션 제목: “전송 층 1 — stdio”client가 server를 자식 process로 직접 띄운다. server는 stdin에서 한 줄에 message 하나를 읽고
stdout에 한 줄에 message 하나를 쓴다. 줄바꿈이 message 경계이므로 message 안에 줄바꿈이 있으면 안 된다.
client → server (stdin): {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}\nserver → client (stdout): {"jsonrpc":"2.0","id":1,"result":{...}}\nserver → client (stderr): [debug] loaded 12 tools ← log 전용, message 아님이 구조가 뜻하는 것 세 가지다.
- 포트가 없다. network가 아니라 pipe다. 같은 machine, 같은 사용자 권한 안에서만 성립한다.
stdout은 message 전용이다. server code가 디버깅용으로print를 찍으면 client의 JSON 파서가 깨진다. log는stderr로 보낸다. stdio server가 “붙자마자 죽는” 원인의 대부분이 이것이다.- 인증이 없다. 스펙은 stdio에서 OAuth를 쓰지 말고 환경 변수 등 실행 환경에서 자격증명을 얻으라고 한다. 누가 이 process를 띄웠는지가 곧 인증이다.
종료도 pipe로 한다. client가 stdin을 닫고 기다렸다가 SIGTERM, 그래도 안 끝나면 SIGKILL이다.
Kubernetes 안에서 stdio server를 쓰려면 누군가 이 pipe의 client 쪽에 서서 network로 바꿔 줘야 하고,
그것이 앞서 본 transport bridge와 kmcp의 sidecar다.
전송 층 2 — Streamable HTTP
섹션 제목: “전송 층 2 — Streamable HTTP”server가 독립 process로 떠서 HTTP endpoint 하나(관례로 /mcp)를 연다. 이름의 “Streamable”은
응답이 필요하면 stream이 될 수 있다는 뜻이다.
| 동작 | 규칙 (2025-11-25 revision) |
|---|---|
| message 보내기 | request·notification·response 하나마다 새 POST. Accept: application/json, text/event-stream 필수 |
| request의 응답 | Content-Type: application/json으로 JSON 하나, 또는 text/event-stream으로 SSE stream. client는 둘 다 받아야 한다 |
| notification의 응답 | body 없는 202 Accepted |
| server → client 먼저 말하기 | client가 GET으로 SSE stream을 열어 두면 server가 그 위로 request·notification을 보낸다 |
| session | server가 initialize 응답에 Mcp-Session-Id를 주면 client는 이후 모든 요청에 같은 header를 붙인다 |
| version | 초기화 뒤 모든 요청에 MCP-Protocol-Version: 2025-11-25 header |
| 보안 | server는 Origin header를 검증해 DNS rebinding을 막고, 로컬이면 127.0.0.1에만 bind |
SSE는 별도 전송이 아니다. 같은 POST의 응답이 “JSON 하나”냐 “event 여러 개”냐의 차이이고, server가
진행 알림이나 중간 요청을 보내고 싶을 때 stream을 고른다. 2024-11-05 revision의 “HTTP+SSE” 전송은
SSE endpoint와 POST endpoint가 따로 있던 옛 방식이고 지금은 deprecated다. 오래된 client가 GET으로
먼저 붙어 endpoint event를 기다리면 그 흔적이다.
POST /mcpAccept: application/json, text/event-streamMcp-Session-Id: 1868a90c…MCP-Protocol-Version: 2025-11-25
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"fetch","arguments":{"url":"https://example.com"}}}HTTP/1.1 200 OKContent-Type: text/event-stream
event: messagedata: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"p1","progress":1,"total":2}}
event: messagedata: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"<!doctype html>…"}],"isError":false}}두 전송을 한 표로 두면 어느 쪽을 골라야 하는지가 드러난다.
| stdio | Streamable HTTP | |
|---|---|---|
| server 위치 | client와 같은 machine의 자식 process | 어디든. network 너머 |
| 다중 client | 불가. process당 client 하나 | 가능 |
| 인증 | 실행 환경 (환경 변수·파일 권한) | OAuth 2.1 (spec의 authorization 절) |
| 잘 맞는 곳 | IDE·데스크톱 앱이 로컬 tool을 붙일 때 | 플랫폼이 배포·공유하는 server |
| 이 덱의 자리 | creator의 로컬 개발. production은 sidecar로 감쌈 | ToolVersion의 production 기본 |
초기화 — version과 capability를 먼저 맞춘다
섹션 제목: “초기화 — version과 capability를 먼저 맞춘다”어느 전송이든 첫 대화는 initialize다. client가 지원하는 version과 자기 capability를 보내고, server가
자기 version·capability·instructions를 돌려주며, client가 notifications/initialized로 마친다.
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ "protocolVersion":"2025-11-25", "capabilities":{"roots":{"listChanged":true},"sampling":{},"elicitation":{"form":{}}}, "clientInfo":{"name":"kagent-engine","version":"0.9.9"}}}{"jsonrpc":"2.0","id":1,"result":{ "protocolVersion":"2025-11-25", "capabilities":{"tools":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"prompts":{}}, "serverInfo":{"name":"mcp-website-fetcher","version":"1.2.0"}, "instructions":"Use fetch only for public HTTPS URLs."}}version 협상 규칙은 단순하다. server가 요청받은 version을 지원하면 같은 값을, 아니면 자기가 지원하는
최신 값을 돌려주고, client가 그것을 모르면 끊는다. capability는 선언한 것만 쓴다 — server가 tools를
선언하지 않았으면 tools/list를 보내지 않고, client가 sampling을 선언하지 않았으면 server는 LLM
호출을 부탁하지 않는다.
| 쪽 | capability | 뜻 |
|---|---|---|
| server | tools | 모델이 호출할 수 있는 함수. listChanged면 목록 변경 알림을 보낸다 |
| server | resources | 읽을 수 있는 데이터(URI로 식별). subscribe면 개별 변경 구독 가능 |
| server | prompts | 재사용 prompt template |
| server | logging·completions | 구조화 log 전송, 인자 자동완성 |
| client | roots | server에게 알려 주는 파일시스템 경계 |
| client | sampling | server가 client의 LLM에게 생성을 부탁할 수 있음 |
| client | elicitation | server가 사용자에게 추가 입력을 요청할 수 있음 |
tools — 발견하고 호출한다
섹션 제목: “tools — 발견하고 호출한다”이 덱이 가장 많이 쓰는 기능이다. 두 method면 끝난다.
tools/list — 이름·설명·inputSchema(JSON Schema 2020-12)를 돌려준다. kagent가 MCPServer에 붙어
발견하는 것이 이 결과이고, toolNames allowlist는 이 name들의 부분집합이다.
{"jsonrpc":"2.0","id":3,"result":{"tools":[ {"name":"fetch","description":"Fetch a URL and return its content", "inputSchema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}, "annotations":{"readOnlyHint":true,"openWorldHint":true}}]}}tools/call — name과 arguments를 보내면 content 배열이 돌아온다. content block은 text·image·
audio·resource_link·resource(embedded) 다섯 종류이고, outputSchema를 선언한 tool은 structuredContent에
JSON을 함께 준다.
오류가 두 층이라는 점을 놓치면 안 된다. 없는 tool 이름이나 schema 위반은 JSON-RPC error(protocol
error)로 오고, tool은 실행됐지만 업무적으로 실패한 것은 result.isError: true에 설명 text로 온다. 후자는
모델이 읽고 인자를 고쳐 재시도하라는 뜻이라 client는 그대로 모델에게 넘긴다.
{"jsonrpc":"2.0","id":4,"result":{ "content":[{"type":"text","text":"Blocked: private address 10.0.0.1 is not allowed"}], "isError":true}}annotations의 readOnlyHint·destructiveHint·idempotentHint·openWorldHint는 server의 자기 신고다.
스펙이 “신뢰하는 server가 아니면 untrusted로 본다”고 못 박으므로, 7장이 ToolCapability의 risk를
discovery 결과가 아니라 검증 단계에서 따로 기록하는 이유가 여기 있다.
revision이 바뀌면 무엇이 달라지나
섹션 제목: “revision이 바뀌면 무엇이 달라지나”MCP는 날짜로 version을 매긴다. 이 덱이 만나는 것은 다섯이다.
| revision | 기억할 것 |
|---|---|
| 2024-11-05 | 첫 공개. HTTP+SSE 전송(endpoint 둘). 지금은 deprecated |
| 2025-03-26 | Streamable HTTP 도입. endpoint 하나, Mcp-Session-Id |
| 2025-06-18 | MCP-Protocol-Version header, structured tool output, elicitation, OAuth resource server 모델 |
| 2025-11-25 | tasks(실험)·URL mode elicitation·icons·Client ID Metadata Documents. kagent 0.9.x가 만나는 최신 |
| 2026-07-28 | stateless로 전환. initialize·session·GET stream·Last-Event-ID 제거, 요청마다 _meta에 version·capability, Mcp-Method·Mcp-Name header 필수. roots·sampling·logging deprecated |
2026-07-28은 앞의 넷과 모양이 다르다. 초기화 handshake가 없으니 위의 initialize 예시가 그대로 통하지
않고, server가 먼저 말할 통로(GET stream)가 없어져 sampling·elicitation 같은 server 발 요청은 결과 안에
input_required로 담아 client가 재시도하는 방식(Multi Round-Trip Request)으로 바뀌었다. 두 era가 섞이는
동안 client는 새 방식으로 먼저 시도하고 400의 body를 보고 옛 방식으로 내려가야 한다. 이 호환 부담이
proxy 장의 새 책임이다.
resources·prompts·client 기능은 어디에 쓰나
섹션 제목: “resources·prompts·client 기능은 어디에 쓰나”tools 밖의 기능도 같은 JSON-RPC 모양이다. 이 덱에서의 쓰임만 짚는다.
| 기능 | method | 이 덱에서 |
|---|---|---|
| resources | resources/list·resources/read·resources/subscribe | URI로 식별하는 읽기 데이터. 3장의 KnowledgeBinding을 MCP로 노출한다면 여기 |
| prompts | prompts/list·prompts/get | server가 제공하는 prompt template. 구성형 Agent의 prompt와는 다른 층 |
| sampling | sampling/createMessage (server → client) | server가 client의 model을 빌려 쓴다. 플랫폼에서는 LiteLLM 경로를 우회하므로 보통 끈다 |
| elicitation | elicitation/create (server → client) | server가 사용자 입력을 요청. 5장의 HITL 지점 후보 |
| roots | roots/list (server → client) | IDE 맥락의 파일 경계. 서버 측 Agent에는 거의 무관 |
- MCP는 세 층이다. JSON-RPC 2.0 message, 그것을 나르는 stdio·Streamable HTTP, 그 위의 tools·resources·prompts와 client 쪽 roots·sampling·elicitation. 포트·프레임워크는 어느 층에도 없다.
- stdio는 자식 process와 pipe라 포트도 인증도 없고
stdout은 message 전용이다. network에 내놓으려면 bridge가 필요하다. - Streamable HTTP는 endpoint 하나에 POST마다 message 하나, 응답은 JSON 또는 SSE다. SSE는 별도 전송이 아니다.
- 첫 대화는
initialize의 version·capability 협상이고 선언한 기능만 쓴다. 2026-07-28부터는 이 handshake가 없다. - tools는
tools/list로 발견하고tools/call로 부르며, 오류는 protocol error와isError두 층이다. kagent의toolNames는 발견된 이름의 부분집합이다.
참고 자료
섹션 제목: “참고 자료”- Base Protocol (2025-11-25) — JSON-RPC message 세 종류,
id규칙,_meta. - Lifecycle (2025-11-25) —
initialize예시, version·capability 협상, 종료 절차. - Transports (2025-11-25) — stdio 규칙, Streamable HTTP의 POST·GET·SSE·session·header.
- Tools (2025-11-25) —
tools/list·tools/call, content 종류,isError, annotations. - Key Changes 2026-07-28 — stateless 전환의 전체 목록.
- JSON-RPC 2.0 Specification — MCP가 그대로 따르는 원본.