MCP·Skill 내부 구조 실습 예시
JSON-RPC 메시지, Skill 점진 공개, transport 선택을 직접 들여다보며 내부 동작을 익히는 가이드 | ← 레퍼런스 문서로 돌아가기
JSON-RPC 2.0 메시지다. 핸드셰이크(initialize), 도구 목록 조회(tools/list), 도구 실행(tools/call) 세 메시지만 읽을 수 있으면 나머지 통신은 같은 패턴의 반복이다.
-
initialize — 연결을 여는 첫 요청·응답
Host의 Client가 가장 먼저 보내는 메시지다.
protocolVersion과capabilities를 제시하면, 서버가 자신이 지원하는 버전과 능력으로 응답하며 협상이 끝난다.id가 있는 요청이므로 반드시 같은id를 가진 응답이 돌아온다.// 요청 (Client → Server) { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": { "sampling": {}, "roots": { "listChanged": true } }, "clientInfo": { "name": "claude-code", "version": "2026.06" } } } // 응답 (Server → Client) { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-06-18", "capabilities": { "tools": { "listChanged": true } }, "serverInfo": { "name": "weather-server", "version": "1.0.0" } } } -
tools/list — 서버가 제공하는 도구 목록 받기
협상이 끝나면 Client는 서버가 어떤 도구를 노출하는지 묻는다. 응답의
tools배열에는 각 도구의name, 설명, 그리고 입력 형식을 정의한inputSchema가 담긴다. 이 스키마가 모델이 매 요청마다 참조하는 "사용 설명서"다.// 요청 { "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} } // 응답 { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_weather", "description": "도시 이름으로 현재 날씨를 조회한다", "inputSchema": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } ] } } -
tools/call — 도구를 실제로 실행하기
모델이 도구를 쓰기로 정하면
tools/call로name과arguments를 보낸다. 서버가 함수를 실행하고 결과를result.content에 담아 돌려준다. 여기까지가 한 번의 도구 호출 왕복이며, 이후의 모든 호출도 같은 골격을 반복한다.// 요청 { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "city": "Seoul" } } } // 응답 { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "서울: 맑음, 24°C" } ] } }
jsonrpc는 항상 "2.0"으로 고정된다. id는 요청과 응답을 짝짓는 번호이며, 응답에는 성공 시 result가, 실패 시 error가 들어간다(둘은 함께 오지 않는다). method는 호출할 동작 이름, params는 그 인자다. 참고로 진행 알림 같은 notification 메시지는 응답을 기대하지 않으므로 id 자체가 없다 — id의 유무가 "요청"과 "알림"을 가르는 핵심 신호다.
initialize → tools/list → tools/call 순서로 고정돼 있다. 직접 눈으로 확인하고 싶다면 G-1을 읽은 뒤 P-1의 MCP Inspector로 같은 메시지를 실시간으로 띄워보면 개념이 바로 손에 잡힌다.
~/.claude/skills/와 프로젝트 .claude/skills/에 스킬 수십~수백 개를 쌓아두었다. "스킬을 많이 깔면 컨텍스트가 터지지 않나?"라는 우려가 흔하지만, Agent Skill의 progressive disclosure(점진 공개)는 단계마다 필요한 만큼만 컨텍스트에 올린다. 한 번의 사용자 발화가 들어왔을 때 무엇이 언제 로드되는지를 단계별로 따라가 본다.
-
Level 1 — 메타데이터만 상시 로드
스킬을 100개 설치해도 세션 시작 시 컨텍스트에 올라가는 것은 각 스킬 frontmatter의
name과description뿐이다. 스킬당 약 100토큰 수준이라, 100개라도 본문 전체가 아니라 "목차"만 모델이 알고 있는 상태다.# 컨텍스트에 들어간 것 (스킬 100개 = 메타 100개) - pdf-report: "재무 PDF에서 표를 추출해 요약. 사용자가 PDF 분석을 요청할 때 사용." - commit-helper: "변경 diff로 커밋 메시지를 작성. 커밋을 만들 때 사용." - ... (98개 더, 각 name + description 한 줄) # 아직 컨텍스트에 없는 것 - 각 SKILL.md 본문, reference/*.md, scripts/*.py
-
발화 → description 매칭 → Level 2 본문 로드
사용자가 "이 분기 재무 PDF에서 핵심 표만 뽑아줘"라고 말하면, 모델은 상시 로드된 description들과 발화를 대조해
pdf-report를 고른다. 이때 비로소 그 스킬의 SKILL.md 본문(보통 5K토큰 미만)이 컨텍스트에 올라간다. 나머지 99개 스킬의 본문은 여전히 로드되지 않는다.# 사용자 발화 "이 분기 재무 PDF에서 핵심 표만 뽑아줘" # 매칭 결과 → 컨텍스트에 추가로 들어간 것 SKILL.md (pdf-report) 본문 전체 ≈ 5K 토큰 미만 - 절차 단계, 주의사항, reference/scripts 안내 # 여전히 컨텍스트에 없는 것 - 나머지 99개 스킬 본문, pdf-report의 reference/, scripts/
-
Level 3 — reference는 조건부, 스크립트는 출력만
SKILL.md 본문이 "복잡한 표 구조는
reference/tables.md를 참고하라"고 안내하면, 모델은 그 조건에 해당할 때만 해당 파일을 읽어 컨텍스트에 올린다. 번들된scripts/extract.py는 코드 자체가 컨텍스트에 적재되지 않고, bash로 실행한 뒤 그 출력만 컨텍스트에 들어온다 — 이 점이 토큰을 가장 크게 아끼는 지점이다.# 조건부로만 로드 reference/tables.md → 본문이 가리킬 때만 읽어서 컨텍스트에 추가 # 스크립트: 코드는 컨텍스트에 안 올라감, 실행 출력만 들어옴 $ python scripts/extract.py report.pdf → stdout: "표 3개 추출 완료: 매출/비용/현금흐름" (extract.py 200줄의 소스 코드는 컨텍스트에 적재되지 않음)
inputSchema 정의가 매 요청 컨텍스트에 상주하는 반면, 스킬은 메타데이터만 상주한다는 점이 둘의 결정적 차이다.
description 문제다. description은 "무엇을 + 언제 사용"을 3인칭으로, 사용자가 실제로 쓸 트리거 단어를 담아 구체적으로 적어야 한다. "보고서 도우미"처럼 모호하면 매칭이 안 되고, 반대로 본문을 description에 욱여넣어 1024자 한도를 넘기면 길이 한도 초과로 스킬이 검증에서 거부되어 로드되지 않는다. 트리거가 안 되면 claude --debug로 YAML 파싱과 매칭 과정을 먼저 확인하라.
stdio), 공용 엔드포인트로 원격 호스팅할지(Streamable HTTP)다. transport는 MCP의 Transport Layer 선택일 뿐 Data Layer(JSON-RPC)는 동일하므로, 고른 뒤 MCP Inspector로 핸드셰이크와 capability가 똑같이 성립하는지 검증한다.
-
선택 기준 정하기 — 로컬 stdio vs 원격 HTTP
stdio는 Claude Code가 서버를 자식 프로세스로 띄워 표준입출력으로 통신한다. 같은 머신·단일 사용자·파일/로컬 명령 접근에 적합하고 네트워크 인증이 필요 없다. 반대로Streamable HTTP는 원격 엔드포인트에 붙어 SSE로 스트리밍하며, 여러 사용자가 한 서버를 공유하거나 클라우드 SaaS에 둘 때 쓰고 OAuth/Bearer 같은 인증이 따라온다.# 로컬 stdio — 같은 머신에서 서버를 자식 프로세스로 실행 claude mcp add weather -- python /path/to/weather_server.py # 원격 Streamable HTTP — 호스팅된 엔드포인트에 연결 claude mcp add --transport http weather https://mcp.example.com/mcp
-
등록 확인 — list / get으로 스코프와 transport 점검
붙인 서버가 어떤 스코프(local/project/user)와 transport로 잡혔는지 확인한다. 팀과 공유하려면 프로젝트 스코프로 등록되어
.mcp.json에 기록돼야 다른 구성원도 같은 설정을 받는다.claude mcp list claude mcp get weather # transport, command/url, scope가 의도대로인지 확인
-
MCP Inspector로 핸드셰이크·capability 검증
Inspector는 서버를 띄워
initialize핸드셰이크를 수행하고 협상된protocolVersion과capabilities를 보여준다. 이어서tools/list로 노출 도구를,tools/call로 실제 실행 결과를 GUI에서 눌러 확인할 수 있어, Claude에 붙이기 전 서버 자체가 정상인지 격리해 검증한다.# 로컬 서버를 직접 띄워 검사 npx @modelcontextprotocol/inspector python /path/to/weather_server.py # 확인 포인트 # - initialize 응답의 protocolVersion / capabilities # - tools/list 에 의도한 도구가 모두 나오는지 # - tools/call 결과(content)가 기대대로인지 # 문제 시 로그: ~/Library/Logs/Claude/mcp*.log
initialize/tools/list/tools/call)는 동일하다. 달라지는 것은 그 메시지를 실어 나르는 통로뿐이다 — stdio는 자식 프로세스의 표준입출력, Streamable HTTP는 인증 헤더가 붙은 HTTP/SSE 스트림이다. 그래서 "로컬에서 Inspector로 통과한 서버는 원격으로 옮겨도 Data Layer는 그대로"라는 전제가 성립한다.
169.254.169.254) 접근을 차단하라. 도구 설명(description/inputSchema)에 악성 지시가 숨는 prompt injection, OAuth confused deputy도 원격 환경에서 현실적인 위협이다.
inputSchema가 매 요청 컨텍스트를 잠식하지 않게 한다.
Claude Docs · Claude Code 2026-06 기준 (MCP 스펙 2025-11-25 / 프로토콜 2025-06-18) | 2026년 6월 27일 작성
← 내부 구조 레퍼런스