MCP·Skill 내부 구조 실습 예시

JSON-RPC 메시지, Skill 점진 공개, transport 선택을 직접 들여다보며 내부 동작을 익히는 가이드  |  ← 레퍼런스 문서로 돌아가기

대상 버전Claude Docs · Claude Code 2026-06 기준 (MCP 스펙 2025-11-25 / 프로토콜 2025-06-18)
이 파일의 목적레퍼런스의 개념(JSON-RPC·primitives·progressive disclosure·transport)을 실제 메시지와 명령으로 재현해 손에 익힌다
레퍼런스 문서internals_Manual.html — 각 시나리오에서 링크로 연결됩니다
작성일2026년 6월 27일
● 초급 Getting Started MCP가 주고받는 실제 메시지를 한 줄씩 뜯어보며 프로토콜의 뼈대를 이해한다
G-1 초급 JSON-RPC 메시지 읽어보기
MCP 서버를 막 붙여본 개발자가 "Claude와 서버 사이에 실제로 어떤 글자가 오가는가"를 알고 싶어 한다. MCP의 Data Layer는 모두 JSON-RPC 2.0 메시지다. 핸드셰이크(initialize), 도구 목록 조회(tools/list), 도구 실행(tools/call) 세 메시지만 읽을 수 있으면 나머지 통신은 같은 패턴의 반복이다.
단계별 실행
  1. initialize — 연결을 여는 첫 요청·응답

    Host의 Client가 가장 먼저 보내는 메시지다. protocolVersioncapabilities를 제시하면, 서버가 자신이 지원하는 버전과 능력으로 응답하며 협상이 끝난다. 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" }
      }
    }
  2. 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"]
            }
          }
        ]
      }
    }
  3. tools/call — 도구를 실제로 실행하기

    모델이 도구를 쓰기로 정하면 tools/callnamearguments를 보낸다. 서버가 함수를 실행하고 결과를 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의 유무가 "요청"과 "알림"을 가르는 핵심 신호다.
세 메시지의 흐름은 initializetools/listtools/call 순서로 고정돼 있다. 직접 눈으로 확인하고 싶다면 G-1을 읽은 뒤 P-1의 MCP Inspector로 같은 메시지를 실시간으로 띄워보면 개념이 바로 손에 잡힌다.
● 중급 Daily Workflow 스킬이 많아져도 컨텍스트가 가벼운 이유 — progressive disclosure가 단계마다 무엇을 로드하는지 추적한다
D-1 중급 Skill 로딩 추적
팀이 ~/.claude/skills/와 프로젝트 .claude/skills/에 스킬 수십~수백 개를 쌓아두었다. "스킬을 많이 깔면 컨텍스트가 터지지 않나?"라는 우려가 흔하지만, Agent Skill의 progressive disclosure(점진 공개)는 단계마다 필요한 만큼만 컨텍스트에 올린다. 한 번의 사용자 발화가 들어왔을 때 무엇이 언제 로드되는지를 단계별로 따라가 본다.
단계별 실행
  1. Level 1 — 메타데이터만 상시 로드

    스킬을 100개 설치해도 세션 시작 시 컨텍스트에 올라가는 것은 각 스킬 frontmatter의 namedescription뿐이다. 스킬당 약 100토큰 수준이라, 100개라도 본문 전체가 아니라 "목차"만 모델이 알고 있는 상태다.

    # 컨텍스트에 들어간 것 (스킬 100개 = 메타 100개)
    - pdf-report: "재무 PDF에서 표를 추출해 요약. 사용자가 PDF 분석을 요청할 때 사용."
    - commit-helper: "변경 diff로 커밋 메시지를 작성. 커밋을 만들 때 사용."
    - ... (98개 더, 각 name + description 한 줄)
    
    # 아직 컨텍스트에 없는 것
    - 각 SKILL.md 본문, reference/*.md, scripts/*.py
  2. 발화 → description 매칭 → Level 2 본문 로드

    사용자가 "이 분기 재무 PDF에서 핵심 표만 뽑아줘"라고 말하면, 모델은 상시 로드된 description들과 발화를 대조해 pdf-report를 고른다. 이때 비로소 그 스킬의 SKILL.md 본문(보통 5K토큰 미만)이 컨텍스트에 올라간다. 나머지 99개 스킬의 본문은 여전히 로드되지 않는다.

    # 사용자 발화
    "이 분기 재무 PDF에서 핵심 표만 뽑아줘"
    
    # 매칭 결과 → 컨텍스트에 추가로 들어간 것
    SKILL.md (pdf-report) 본문 전체  ≈ 5K 토큰 미만
      - 절차 단계, 주의사항, reference/scripts 안내
    
    # 여전히 컨텍스트에 없는 것
    - 나머지 99개 스킬 본문, pdf-report의 reference/, scripts/
  3. 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줄의 소스 코드는 컨텍스트에 적재되지 않음)
세 단계를 합치면 "스킬 수 × 본문 크기"가 아니라 "스킬 수 × 메타 100토큰 + 실제로 발동한 1개의 본문"만이 컨텍스트 비용이 된다. 이것이 스킬을 아무리 많이 설치해도 상시 비용이 거의 늘지 않는 이유다. 같은 자기완결 번들이라도 MCP 도구는 inputSchema 정의가 매 요청 컨텍스트에 상주하는 반면, 스킬은 메타데이터만 상주한다는 점이 둘의 결정적 차이다.
Level 2가 끝내 로드되지 않는다면 거의 항상 description 문제다. description은 "무엇을 + 언제 사용"을 3인칭으로, 사용자가 실제로 쓸 트리거 단어를 담아 구체적으로 적어야 한다. "보고서 도우미"처럼 모호하면 매칭이 안 되고, 반대로 본문을 description에 욱여넣어 1024자 한도를 넘기면 길이 한도 초과로 스킬이 검증에서 거부되어 로드되지 않는다. 트리거가 안 되면 claude --debug로 YAML 파싱과 매칭 과정을 먼저 확인하라.
● 고급 Power User 로컬 stdio냐 원격 Streamable HTTP냐 — transport를 골라 붙이고 Inspector로 핸드셰이크를 검증한다
P-1 고급 transport 선택과 디버깅
사내 도구를 MCP 서버로 노출해 여러 사람이 쓰게 하려는 숙련자가 두 갈래에서 고민한다. 개발 머신에서 로컬로 돌릴지(stdio), 공용 엔드포인트로 원격 호스팅할지(Streamable HTTP)다. transport는 MCP의 Transport Layer 선택일 뿐 Data Layer(JSON-RPC)는 동일하므로, 고른 뒤 MCP Inspector로 핸드셰이크와 capability가 똑같이 성립하는지 검증한다.
단계별 실행
  1. 선택 기준 정하기 — 로컬 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
  2. 등록 확인 — list / get으로 스코프와 transport 점검

    붙인 서버가 어떤 스코프(local/project/user)와 transport로 잡혔는지 확인한다. 팀과 공유하려면 프로젝트 스코프로 등록되어 .mcp.json에 기록돼야 다른 구성원도 같은 설정을 받는다.

    claude mcp list
    claude mcp get weather
    # transport, command/url, scope가 의도대로인지 확인
  3. MCP Inspector로 핸드셰이크·capability 검증

    Inspector는 서버를 띄워 initialize 핸드셰이크를 수행하고 협상된 protocolVersioncapabilities를 보여준다. 이어서 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
transport를 바꿔도 Inspector에 찍히는 JSON-RPC 메시지(G-1의 initialize/tools/list/tools/call)는 동일하다. 달라지는 것은 그 메시지를 실어 나르는 통로뿐이다 — stdio는 자식 프로세스의 표준입출력, Streamable HTTP는 인증 헤더가 붙은 HTTP/SSE 스트림이다. 그래서 "로컬에서 Inspector로 통과한 서버는 원격으로 옮겨도 Data Layer는 그대로"라는 전제가 성립한다.
원격 Streamable HTTP로 전환하는 순간 공격면이 넓어진다. 인증(OAuth/Bearer/API key)을 반드시 걸고, 비결정적 session id로 세션 하이재킹을 막아야 한다. 특히 서버가 임의 URL로 요청을 보낼 수 있다면 SSRF에 주의해 내부 IP와 클라우드 메타데이터 주소(169.254.169.254) 접근을 차단하라. 도구 설명(description/inputSchema)에 악성 지시가 숨는 prompt injection, OAuth confused deputy도 원격 환경에서 현실적인 위협이다.
검증 순서를 "Inspector(격리) → 로컬 stdio 등록 → 원격 HTTP 전환" 으로 두면 문제 구간을 좁히기 쉽다. Inspector에서 통과했는데 Claude에서만 실패하면 등록 스코프나 인증 헤더를, 둘 다 실패하면 서버 자체를 의심한다. 불필요한 도구는 비활성화해 inputSchema가 매 요청 컨텍스트를 잠식하지 않게 한다.

Claude Docs · Claude Code 2026-06 기준 (MCP 스펙 2025-11-25 / 프로토콜 2025-06-18) | 2026년 6월 27일 작성
← 내부 구조 레퍼런스