byteforce

CPN 한국어 자습서 · Introduction to Model Context Protocol

1 · Introduction

MCP 클라이언트

MCP clients

MCP에서 다음으로 볼 부분은 클라이언트입니다. 클라이언트는 우리 서버와 MCP 서버 사이의 통신 수단이자, 그 서버가 구현한 도구들로의 접근점입니다. 연결 방식(transport)과 주고받는 메시지를 살펴보고, 마지막에 전체 호출 흐름을 한 단계씩 따라갑니다.

전체 내레이션영상 나레이션 한국어 번역 (전체)

Stephen Grider · Anthropic 기술 스태프

모델 컨텍스트 프로토콜에서 다음으로 살펴볼 부분은 클라이언트입니다. 클라이언트의 목적은 여러분의 서버와 MCP 서버 사이의 통신 수단을 제공하는 것입니다. 이 클라이언트는 그 서버가 구현한 모든 도구로의 접근점이 됩니다.

MCP는 transport agnostic입니다. 멋진 용어이지만 뜻은 단순합니다 — 클라이언트와 서버가 여러 가지 프로토콜로 통신할 수 있다는 말입니다. 지금 MCP 서버를 돌리는 아주 흔한 방식은 MCP 클라이언트와 같은 물리적 머신에서 돌리는 것입니다. 둘이 같은 머신에 있으면 표준 입출력(standard input output)으로 통신할 수 있고, 이 섹션에서 나중에 그렇게 설정합니다.

물론 MCP 클라이언트와 MCP 서버를 잇는 다른 방법도 있습니다. HTTP나 WebSockets, 그 밖에 여러 방식이나 기법으로도 연결할 수 있습니다.

클라이언트와 서버 사이에 연결이 만들어지면, 둘은 메시지를 주고받으며 통신합니다. 어떤 메시지가 허용되는지는 모두 MCP 스펙에 정의돼 있습니다. 우리가 집중할 메시지 유형 몇 가지는 list tools request와 list tools result입니다. 짐작했듯이 list tools request는 클라이언트가 서버로 보내, 서버가 제공하는 모든 도구를 나열해 달라고 요청합니다. 그러면 서버는 자신이 제공할 수 있는 모든 도구 목록을 담은 list tools result 메시지로 응답합니다.

우리가 보게 될 또 다른 흔한 메시지 유형은 call tool request와 call tool result입니다. 앞의 것은 특정 인자로 도구를 실행해 달라고 서버에 요청하고, 뒤의 것은 그 도구 실행 결과를 담습니다.

지금 시점에 우리는 서버와 클라이언트라는 개념을 갖고 있지만, 이 모든 게 실제로 어떻게 함께 작동하는지는 아직 또렷하지 않을 것입니다. 그래서 이 영상의 나머지에서는 여러 요소 사이의 예시 호출을 처음부터 끝까지 따라가 보겠습니다. 다소 복잡한 과정이지만, 우리가 구성하는 서버, MCP 클라이언트, MCP 서버, 데이터를 가져오려는 제공자로서의 GitHub, 그리고 Claude 사이에 오가는 통신을 상상해 봅니다.

먼저 사용자가 우리 서버에 “내 저장소가 뭐가 있어?” 같은 질의를 제출합니다. 이제 우리 서버는 Claude에 요청을 보내야 하는데, 그 요청에는 Claude가 쓸 수 있는 모든 도구를 함께 나열해 넣고 싶습니다. 그래서 Claude에 요청하기 전에, 서버는 MCP 클라이언트와 서버를 거치는 작은 우회를 합니다. 서버는 Claude에 보낼 도구 목록이 필요하다는 걸 깨닫고, MCP 클라이언트에 도구 목록을 달라고 요청합니다. MCP 클라이언트는 이어 list tools request를 서버로 보내고, 서버는 list tools result로 응답합니다. 이제 MCP 클라이언트가 도구 목록을 갖게 됐으니, 그 목록을 다시 우리 서버에 돌려줍니다.

이제 우리 서버에는 사용자의 원래 메시지와 포함할 도구 목록이 모두 있습니다. 그래서 서버는 그 질의와 도구 묶음을 담아 Claude에 요청합니다. Claude는 도구를 보고 “이 질문에 답하려면 도구를 호출하고 싶다”고 판단해, tool use 메시지 파트로 응답합니다. 우리 서버는 Claude가 도구를 실행하길 원한다는 걸 알아채지만, 이제 도구 실행은 우리 서버가 맡지 않습니다. 도구는 MCP 서버가 실행합니다.

그래서 Claude가 요청한 도구를 실행하기 위해, 우리 서버는 Claude가 준 특정 인자로 도구를 실행해 달라고 MCP 클라이언트에 요청합니다. MCP 클라이언트는 실제로 도구를 실행하지 않고, call tool request를 MCP 서버로 보냅니다. MCP 서버는 그 요청을 받아 GitHub로 후속 요청을 보냅니다 — 바로 여기서 이 사용자의 저장소 목록을 실제로 가져옵니다. GitHub가 저장소 목록으로 응답하면, MCP 서버는 그 데이터를 call tool result에 감싸 MCP 클라이언트로 돌려보내고, 클라이언트는 그 결과를 다시 우리 서버에 건넵니다.

이제 우리 서버에는 저장소 목록이 있습니다. 서버는 그 도구 결과를 user 메시지 안의 tool result 파트로 담아 Claude에 후속 요청을 보냅니다. 이제 Claude는 최종 응답을 작성하는 데 필요한 모든 정보를 가졌으니, “당신의 저장소는…” 같은 텍스트를 써서 우리 서버로 보내고, 우리 서버는 그걸 다시 사용자에게 전달합니다. 이 흐름은 꽤 복잡합니다. 이걸 보여 주는 이유는, 조금 뒤에 우리만의 MCP 클라이언트와 MCP 서버를 직접 구현하면서 이 모든 조각을 보게 되기 때문입니다.

이 장에서 배우는 것What you'll learn

약 5분
1

클라이언트 = 우리 서버와 MCP 서버 사이의 통신 수단·도구 접근점

2

transport agnostic — 여러 프로토콜로 통신 가능

3

같은 머신이면 표준 입출력(stdio)(이 섹션에서 사용), 그 외 HTTP·WebSockets

4

메시지로 통신 — list_tools request/result, call_tool request/result

5

전체 흐름 — user→서버→클라이언트→MCP 서버→GitHub→Claude→최종 답

6

이 조각들을 뒤에서 직접 구현하게 됩니다

먼저 짚고 갈 용어
클라이언트
우리 서버와 MCP 서버 사이의 통신 수단. 서버가 구현한 도구들로의 접근점.
transport agnostic
클라이언트·서버가 특정 전송 방식에 묶이지 않고 여러 프로토콜로 통신할 수 있음.
stdio
표준 입출력. 클라이언트와 서버가 같은 머신에 있을 때 흔히 쓰는 연결 방식.
list_tools / call_tool
두 핵심 메시지. list_tools는 도구 목록을 요청/응답, call_tool은 인자로 도구를 실행 요청/결과.
request / result
메시지의 두 방향. request는 요청을, result는 그 응답·결과를 담는다.

클라이언트와 연결 방식

Transport agnostic

클라이언트의 목적은 우리 서버와 MCP 서버 사이의 통신 수단을 제공하는 것입니다. 클라이언트는 그 서버가 구현한 모든 도구로의 접근점이 됩니다. MCP는 transport agnostic — 클라이언트와 서버가 여러 프로토콜로 통신할 수 있다는 뜻입니다. 둘이 같은 머신에 있으면 표준 입출력(stdio)으로 통신하며, 이 섹션에서 그렇게 설정합니다.

stdio

표준 입출력. 클라이언트와 서버가 같은 머신에 있을 때.

이 섹션에서 사용

HTTP

네트워크 너머로 연결할 때 쓰는 방식 중 하나.

WebSockets

그 밖의 여러 방식·기법 중 하나.

주고받는 메시지

list_tools · call_tool

연결이 만들어지면 클라이언트와 서버는 메시지를 주고받으며 통신합니다. 어떤 메시지가 허용되는지는 MCP 스펙에 정의돼 있습니다. 우리가 집중할 두 쌍은 다음과 같습니다.

list_tools

request는 클라이언트가 서버로 보내 도구 목록을 요청하고, result는 서버가 제공 가능한 도구 목록으로 응답합니다.

call_tool

request는 특정 인자로 도구를 실행해 달라고 요청하고, result는 그 도구 실행 결과를 담습니다.

전체 호출 흐름

An example call, end to end

서버와 클라이언트가 실제로 어떻게 맞물리는지, 사용자 질의 “내 저장소가 뭐가 있어?”부터 Claude의 최종 답까지 한 단계씩 따라갑니다. 다소 복잡하지만, 뒤에서 이 조각들을 직접 구현하게 됩니다. 아래에서 단계를 넘기며 어떤 메시지가 누구→누구로 가는지 확인하세요.

전체 호출 흐름 · 한 단계씩 따라가기
1 / 8
사용자
우리 서버
MCP 클라이언트
MCP 서버
GitHub
Claude

실제 도구 실행은 MCP 서버가 합니다(우리 서버가 아니라). list_tools로 도구 목록을, call_tool로 도구 실행을 주고받습니다.

정리 & 점검

Recap & check
핵심 정리
  • 클라이언트 = 우리 서버와 MCP 서버 사이의 통신 수단·도구 접근점.
  • transport agnostic — 같은 머신이면 stdio, 그 외 HTTP·WebSockets.
  • 메시지로 통신 — list_tools(도구 목록) · call_tool(도구 실행), 각각 request/result.
  • 실제 도구 실행은 MCP 서버가 한다(우리 서버가 아니라).

Q1개발 중 MCP 클라이언트와 서버를 잇는 가장 흔한 방식은?

Q2Claude가 요청한 도구를 실제로 실행하는 주체는?


LAB · 실습 콘솔MCP MESSAGE ROUNDTRIP

MCP 메시지 왕복

클라이언트와 서버가 주고받는 두 메시지(list_tools · call_tool)를 로컬에서 그대로 재현해, 도구 하나가 실행되기까지 오가는 왕복을 한 줄씩 따라갑니다.

index.js
// MCP 메시지 왕복 — MCP 클라이언트와 서버는 정해진 메시지로만 대화한다.
// list_tools(도구 목록)와 call_tool(도구 실행) 왕복을 로컬에서 그대로 재현한다.
// Claude API·네트워크 없이, 서버가 어떤 순서로 메시지를 주고받는지만 본다.

// ── MCP 서버 — 도구 둘을 가진 메모리 서버(2장의 그 docs 예시 그대로) ──
const docs = {
  "report.pdf": "1분기 매출은 전년 대비 12% 늘었다.",
  "plan.md": "다음 스프린트 목표: 온보딩 개선.",
};

const server = {
  // 서버가 제공하는 도구 목록 — list_tools 요청에 이 스키마를 돌려준다.
  tools: {
    read_document: {
      description: "문서 내용을 문자열로 읽어 돌려준다.",
      run: function (args) {
        if (!(args.doc_id in docs)) throw new Error("문서 없음: " + args.doc_id);
        return docs[args.doc_id];
      },
    },
    edit_document: {
      description: "문서에서 old_str 을 new_str 로 찾아 치환한다.",
      run: function (args) {
        if (!(args.doc_id in docs)) throw new Error("문서 없음: " + args.doc_id);
        docs[args.doc_id] = docs[args.doc_id].split(args.old_str).join(args.new_str);
        return "치환 완료: " + args.doc_id;
      },
    },
  },
  // 서버는 들어온 메시지 종류에 따라 응답 메시지를 만든다.
  handle: function (msg) {
    if (msg.method === "list_tools") {
      const list = Object.keys(this.tools).map(function (name) {
        return { name: name, description: server.tools[name].description };
      });
      return { type: "list_tools_result", tools: list };
    }
    if (msg.method === "call_tool") {
      try {
        const out = this.tools[msg.params.name].run(msg.params.args);
        return { type: "call_tool_result", ok: true, content: out };
      } catch (e) {
        return { type: "call_tool_result", ok: false, content: e.message };
      }
    }
    return { type: "error", content: "모르는 메시지: " + msg.method };
  },
};

// ── 전송(transport) — 클라이언트와 서버 사이를 지나는 모든 메시지를 찍는다 ──
let seq = 0;
function send(direction, msg) {
  seq++;
  console.log("[" + seq + "] " + direction + "  " + JSON.stringify(msg));
}

// MCP 클라이언트 — 서버로 요청을 보내고 응답을 받는 통로.
const client = {
  request: function (msg) {
    send("client → server", msg);
    const res = server.handle(msg);
    send("server → client", res);
    return res;
  },
};

// ── 여기서부터 직접 고쳐 보세요 ──
// 어떤 도구를, 어떤 인자로 부를지 바꿔 보세요.
// 예) edit_document 로 바꾸고 args: { doc_id: "report.pdf", old_str: "12%", new_str: "20%" }
const toolCall = { name: "read_document", args: { doc_id: "report.pdf" } };

// 전체 흐름 — ① 도구 목록을 받고 ② 그중 하나를 실행한다.
console.log("── ① 도구 목록 요청 ──");
const listed = client.request({ method: "list_tools" });
console.log("서버가 알린 도구: " +
  listed.tools.map(function (t) { return t.name; }).join(", "));

console.log("");
console.log("── ② 도구 실행 요청 ──");
const called = client.request({ method: "call_tool", params: toolCall });
console.log(called.ok
  ? "결과: " + called.content
  : "실패: " + called.content);