LabHub
배우기 러닝패스 코스

에이전트가 내 DB 를 지웠다 · MCP 가 정한 것 · 이론

MCP 가 정한 것 — 메시지 세 종류와 도구 계약

LabHub 에서 이어서 보기

한 줄 요약

MCP(Model Context Protocol)는 LLM 애플리케이션과 외부 도구·데이터를 잇는 개방
프로토콜이다. 메시지는 JSON-RPC 2.0 이고, 연결은 상태를 가지며, 양쪽이 능력을
협상한다. 이 세 문장이 [스펙 개요](https://modelcontextprotocol.io/specification/2025-06-18)
가 "Key Details" 로 적어 둔 전부이고, 나머지는 그 위에 얹힌 약속이다.

왜 이게 필요했나

에이전트를 하나 만들면 도구가 붙는다. 데이터베이스를 읽는 도구, 파일을 여는
도구, 사내 API 를 부르는 도구. 처음에는 그 도구를 함수 세 개로 만들고 모델에
설명을 붙이면 끝난다. 문제는 두 번째 에이전트 앱을 만들 때, 그리고 다른 팀의
도구를 가져다 쓸 때 생긴다. 도구의 이름·인자·결과 형식·오류 처리·승인 절차를
앱마다 다시 정해야 하고, 도구 쪽도 앱마다 다른 붙이기 코드를 가진다. N 개의 앱과
M 개의 도구가 N×M 개의 어댑터를 낳는다.

스펙은 이 문제를 언어 서버 프로토콜(LSP)에 빗댄다. 편집기마다 언어 지원을 따로
만들던 시절을 LSP 가 "편집기 ↔ 언어 서버" 한 가지 규약으로 끝낸 것처럼, MCP 는
"LLM 앱 ↔ 도구 서버" 를 한 가지 규약으로 끝내려는 것이다. 그래서 스펙이 정의하는
것은 모델의 동작이 아니라 메시지의 모양이다. 누가 먼저 말하고, 무엇을 답해야
하고, 실패를 어떻게 알리는가.

역할은 셋이다. 호스트(Host)는 연결을 시작하는 LLM 앱이고, 클라이언트
그 호스트 안에서 서버 하나와 1:1 로 붙는 커넥터이며, 서버는 맥락과 능력을
제공하는 쪽이다. 서버가 낼 수 있는 것은 세 가지 — 리소스(읽을 데이터), 프롬프트
(템플릿), 그리고 이 코스의 주제인 도구(모델이 실행할 함수)다.

어떻게 동작하나

메시지는 세 종류다. [기본 프로토콜](https://modelcontextprotocol.io/specification/2025-06-18/basic)
은 요청·응답·알림을 정의한다. 요청은 idmethod 를 갖고, 응답은 같은 id
result 또는 error하나만 싣는다. 알림은 id 가 없고, 받은 쪽은
답해서는 안 된다(MUST NOT). JSON-RPC 2.0 원문은 id 에 null 을 허용하지만 MCP 는
한 발 더 나가 null 을 금지하고, 한 세션 안에서 같은 id 를 다시 쓰는 것도 금지한다.

{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "count_orders", "arguments": {"status": "paid"}}}

연결에는 순서가 있다. [라이프사이클](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle)
은 초기화 → 운영 → 종료 세 단계를 정한다. 클라이언트가 initialize 요청에
자기가 지원하는 프로토콜 버전·능력·구현 정보를 담아 보내면, 서버는 자기 버전과
능력(tools, resources, prompts, logging …)과 serverInfo 로 답한다.
서버가 그 버전을 지원하면 같은 버전을, 아니면 자기가 아는 다른 버전을 돌려주고,
클라이언트는 그 버전을 모르면 끊어야 한다(SHOULD). 그 뒤 클라이언트가
notifications/initialized 알림을 보내면 비로소 운영 단계다. 초기화 전에는
양쪽 다 ping 정도 외에는 요청을 보내지 않는 것이 규칙이다.

도구는 두 메서드로 끝난다. [도구 스펙](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)
tools/list 는 도구 목록을 돌려주고, 각 도구는 name·description·
inputSchema(JSON Schema)를 가진다. 모델은 이 스키마를 읽고 인자를 만든다.
tools/callnamearguments 를 받아 content 배열(텍스트·이미지·
오디오·리소스 링크)을 돌려준다. 여기서 중요한 구분이 하나 있다 — 오류가 **두
종류**다. 모르는 도구 이름이나 잘못된 인자처럼 요청 자체가 틀린 것은 JSON-RPC
error(스펙의 예시는 -32602)로 답하고, 도구가 실행되다 실패한 것(API 실패,
비즈니스 규칙 위반)은 result 안에 isError: true 와 설명 텍스트로 답한다.
후자는 모델이 읽고 다시 시도할 수 있어야 하기 때문이다.

전송은 두 가지다. [전송 스펙](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)
의 stdio 는 클라이언트가 서버를 자식 프로세스로 띄워 stdin 으로 요청을 쓰고
stdout 에서 응답을 읽는 방식이다. 메시지는 줄바꿈으로 나뉘고 안에 줄바꿈이
있어서는 안 되며, 서버는 stdout 에 MCP 메시지 외의 것을 써서는 안 되고(MUST NOT),
로그는 stderr 로 쓸 수 있다. Streamable HTTP 는 하나의 엔드포인트가 POST 와 GET
을 받고 필요하면 SSE 로 여러 메시지를 흘리는 방식인데, 로컬에서 돌릴 때는
Origin 헤더를 검증하고 127.0.0.1 에만 묶으라고 못 박는다 — DNS 리바인딩으로
브라우저의 웹 페이지가 내 로컬 MCP 서버를 조종하는 것을 막기 위해서다.

현장에서 만나는 모습

이 코스 제목의 사고는 이렇게 난다. 누군가 사내 DB 를 감싼 MCP 서버를 만들었고,
도구는 run_sql 하나였다. 에이전트가 "테스트 데이터 좀 정리해 줘" 를 받고
DROP TABLE orders 를 만들어 보냈고, 서버는 그것을 실행했다. 모델이 나쁜 것이
아니다. 스펙은 도구를 모델이 고르는 것(model-controlled) 이라 정의하고, 그래서
사람이 거부할 수 있는 자리가 항상 있어야 하며(SHOULD), 서버는 모든 입력을
검증하고 접근 제어를 구현하고 호출 빈도를 제한해야 한다(MUST)고 적는다. 그
문장들이 코드 어디에도 없었던 것이 원인이다.

현장에서 MCP 서버를 볼 때 먼저 확인할 것은 세 가지다. tools/list 가 내는 도구가
얼마나 좁은가(만능 도구가 있으면 사고 예약이다), 파괴적 도구에 확인 절차가
있는가, 그리고 stdout 위생 — 디버그 print 한 줄이 클라이언트의 JSON 파서를
죽여 "서버가 응답하지 않는다" 로 보이는 일이 실제로 잦다. 이 코스의 세 실습이
그 셋을 순서대로 다룬다.

다음 퀴즈에서 확인할 것

메시지 세 종류의 구분(특히 알림에 답하면 안 되는 이유), initialize 의 순서와
버전 협상, 오류 두 종류의 구분, stdio 에서 stdout 과 stderr 의 역할을 묻는다.
전부 이 글에 있는 내용이다.