byteforce

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

3 · Connecting with MCP clients

클라이언트 구현하기

Implementing a client

이제 mcp_client.pyMCPClient 클래스를 봅니다. 이 클래스는 SDK가 주는 ClientSession(MCP 서버와의 실제 연결)을 감싸는데, 세션은 정리(cleanup)가 필요해서 connect·async enter/exit에 정리 코드가 이미 들어 있습니다. 이 클래스는 서버 기능을 우리 코드베이스에 노출합니다 — 이번에는 그중 list_tools·call_tool 두 함수를 구현합니다.

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

Stephen Grider · Anthropic 기술 스태프

서버가 좋은 상태가 됐으니, 방향을 살짝 틀어 MCP 클라이언트 작업을 시작합니다. 클라이언트는 프로젝트 루트의 mcp_client.py 파일에 있습니다. 시작하기 전에 빠른 리마인더 하나. 보통의 프로젝트에서는 클라이언트를 쓰거나, 서버를 구현하거나 둘 중 하나만 합니다. 이번 프로젝트에서만 양쪽을 다 만들고 있는 거예요 — 퍼즐의 두 면을 다 보여 드리려고요.

이 파일 안의 MCP 클라이언트는 클래스 하나로 돼 있습니다. 코드가 꽤 많고, 방금 서버에서 쓴 코드만큼 예쁘지도 않습니다. 왜 이렇게 큰지 정확히 설명드리죠. 이 클래스는 ClientSession이라는 것을 감쌉니다. ClientSession은 우리 MCP 서버로의 실제 연결이고, Python SDK의 일부입니다.

이 세션은 약간의 리소스 정리가 필요합니다. 프로그램을 닫거나 서버가 더는 필요 없을 때, 정리 과정을 거쳐야 하죠. 그 정리 코드 상당 부분을 MCPClient 클래스 안에 이미 써 뒀습니다. 사실 이 클래스가 존재하는 이유가 바로 그 정리를 좀 더 쉽게 하려는 것입니다. connect 함수와 그 아래의 cleanup, async enter, async exit 함수에서 정리 코드를 볼 수 있습니다. ClientSession을 직접 쓰기보다, 이렇게 더 큰 클래스로 감싸 리소스 관리를 맡기는 게 아주 흔한 방식입니다.

이 클라이언트가 대체 무슨 일을 해 주는 걸까요? 앞서 본 전체 흐름을 떠올려 보세요. 어느 시점엔 Claude에 보낼 도구 목록이 필요했고, 그다음엔 Claude가 요청한 도구를 실행해야 했습니다. MCP 서버에 손을 뻗어 도구 목록을 받거나 도구를 실행할 때, 바로 이 MCP 클라이언트를 씁니다. 즉 클라이언트는 서버의 기능을 우리 코드베이스의 나머지 부분에 노출해 줍니다.

이 프로젝트의 core 디렉터리에는 제가 미리 짜 둔 코드가 많은데, 그 코드가 이 클래스를 쓰고 있습니다. list_tools, call_tool, list_prompts, get_prompt 같은 함수들을 호출하죠. 지금 영상에서는 그중 list_tools와 call_tool 두 함수에 집중합니다. 이 둘은 코드베이스 곳곳에서 — Claude에 줄 도구 목록을 얻거나, Claude가 요청한 도구를 실행할 때 — 쓰입니다.

두 함수 구현은 아주 간단합니다. 먼저 list_tools. TODO를 지우고 result = await self.session().list_tools()로 바꾸고, return result.tools를 둡니다. 이게 전부입니다. 세션, 즉 MCP 서버로의 실제 연결에 접근해서, 서버가 구현한 도구들의 정의/목록을 받는 내장 함수를 호출하고, result에서 tools만 돌려주는 거죠.

call_tool도 비슷합니다. return await self.session().call_tool(tool_name, tool_input) 한 줄. 마찬가지로 세션에 접근해, 넘겨받은 이름의 특정 도구를, Claude가 준 입력 인자와 함께 실행하려 시도합니다.

이 두 함수를 빠르게 테스트해 봅니다. 파일 맨 아래에 작은 테스트 하네스를 만들어 뒀습니다. 이 with 블록에서 mcp_client.py를 직접 실행하면, MCP 서버에 연결을 맺고 명령을 돌려 결과를 볼 수 있습니다. (UV를 안 쓰면 command·args를 바꾸라는 주석이 있으니 참고하세요.) 블록 안에 result = await _client.list_tools()print(result)를 넣습니다.

터미널에서 uv run mcp_client.py를 실행합니다(UV를 안 쓰면 python mcp_client.py). 그러면 도구 정의 목록이 나옵니다 — read_document 도구와 edit_document 도구가 보이고, 각각 description과 input schema를 갖고 있습니다. 이게 곧 Claude에 넘어갈 도구 정의죠.

하나 더 테스트합니다. list_tools와 call_tool을 호출하는 다른 코드는 이미 프로젝트 어딘가에 구현해 뒀습니다. 그러니 이제 CLI를 다시 돌려 Claude가 이 도구들을 쓰게 할 수 있습니다. uv run main.py로 프로젝트를 실행하고, "report.pdf 문서 내용이 뭐야?"라고 묻습니다 — report.pdf라고 정확히 넣으세요. 요청과 함께 도구 목록이 전송되고, Claude는 read_document 도구를 쓰기로 결정해 내용을 가져옵니다. 그 보고서가 20미터 응축탑에 관한 것이라는 답을 받게 됩니다. 이렇게 클라이언트로 서버의 도구를 나열하고 실행하는 기능을 더했습니다.

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

약 5분
1

MCPClient = ClientSession(실제 연결)을 감싸는 클래스

2

connect·cleanup·async enter/exit에 정리 코드가 이미 있다

3

클라이언트는 서버 기능을 코드베이스에 노출한다

4

list_toolsself.session().list_tools()result.tools

5

call_toolself.session().call_tool(name, input)

6

uv run mcp_client.py로 도구 정의 출력 → main.py로 Claude 호출

먼저 짚고 갈 용어
MCPClient
우리가 만드는 클래스. ClientSession을 감싸 리소스 정리를 맡고, 서버 기능을 코드베이스에 노출.
ClientSession
SDK가 주는 객체. MCP 서버와의 실제 연결. 직접 쓰기보다 클래스로 감싸는 게 보통.
cleanup (정리)
세션은 닫을 때 리소스 정리가 필요. connect·async enter/exit에 정리 코드가 들어 있다.
list_tools
서버가 정의한 도구 목록을 받는다. Claude에 보낼 도구 정의를 얻을 때 쓴다.
call_tool
서버가 구현한 특정 도구를, 이름·입력 인자와 함께 실행한다. Claude의 tool_use 요청을 처리.
테스트 하네스
파일 하단의 with 블록. mcp_client.py를 직접 실행해 서버에 붙어 동작을 확인.

MCPClient는 무엇을 감싸나

Wrapping ClientSession

서버가 안정됐으니 이제 클라이언트 차례입니다. mcp_client.py의 MCPClient는 클래스 하나로, SDK가 주는 ClientSession(서버와의 실제 연결)을 감쌉니다. 코드가 큰 이유는 세션의 정리(cleanup) 때문 — 그 코드는 이미 들어 있습니다.

왜 클래스로 감싸나

세션은 닫을 때 리소스 정리가 필요합니다. ClientSession을 직접 쓰기보다 더 큰 클래스로 감싸 connect·async enter/exit에서 정리를 맡기는 게 흔한 방식입니다. 또 학습용이라 이 프로젝트는 서버·클라이언트를 둘 다 만들지만, 실무에선 보통 한쪽만 만듭니다.

list_tools · call_tool 구현

Two functions

클라이언트는 서버 기능을 코드베이스에 노출합니다. 이번엔 두 함수만 구현합니다. list_tools는 세션에 도구 목록을 요청해 result.tools를 돌려주고, call_tool은 이름·입력 인자로 도구를 실행합니다.

mcp_client.py · list_tools
async def list_tools(self) -> list[types.Tool]:
    result = await self.session().list_tools()
    return result.tools
mcp_client.py · call_tool
async def call_tool(
    self, tool_name: str, tool_input: dict
) -> types.CallToolResult | None:
    return await self.session().call_tool(tool_name, tool_input)

두 함수 모두 self.session()으로 실제 연결에 접근해 SDK 내장 함수를 부르는 게 전부입니다. 이 둘이 코드베이스 곳곳에서 — 도구 목록을 얻고, Claude가 요청한 도구를 실행할 때 — 호출됩니다.

두 함수가 전체 흐름의 어디에 쓰이는지 · 탭으로 전환

클라이언트는 서버의 기능을 코드베이스에 노출합니다. list_tools는 Claude에 줄 도구 목록을 얻을 때, call_tool은 Claude가 도구 실행을 요청했을 때 호출됩니다 — 강조된 단계가 그 지점입니다.

테스트 하네스로 확인

Test harness

파일 하단의 with 블록에서 mcp_client.py를 직접 실행하면 서버에 붙어 동작을 확인할 수 있습니다. list_tools를 호출해 도구 정의를 출력해 봅니다.

mcp_client.py 하단 · 테스트 하네스
# mcp_client.py 하단 · 작은 테스트 하네스
# (UV를 안 쓰면 command·args를 바꾸라는 주석 참고)
async with MCPClient(
    command="uv",
    args=["run", "mcp_server.py"],
) as _client:
    result = await _client.list_tools()
    print(result)   # 서버가 정의한 도구들을 출력
출력 · uv run mcp_client.py
$ uv run mcp_client.py

[Tool(name='read_document',
      description='Read the contents of a document ...',
      inputSchema={'properties': {'doc_id': {... 'type': 'string'}},
                   'title': 'read_documentArguments', 'type': 'object'}),
 Tool(name='edit_document',
      description='Edit a document by replacing a string ...',
      inputSchema={'properties': {'doc_id': {...}, 'old_str': {...}, 'new_str': {...}},
                   'title': 'edit_documentArguments', 'type': 'object'})]

각 도구가 descriptioninput schema를 갖고 있습니다 — 이게 곧 Claude에 넘어갈 도구 정의입니다. 이제 CLI를 돌려 Claude가 이 도구를 실제로 쓰게 합니다.

출력 · uv run main.py
$ uv run main.py
> report.pdf 문서 내용이 뭐야?

report.pdf 문서의 내용을 가져왔습니다. 이 보고서는
20미터 응축탑의 상태에 관한 내용입니다 ...
핵심 정리
  • MCPClient는 ClientSession(실제 연결)을 감싸 정리를 맡고 서버 기능을 노출한다.
  • list_toolsself.session().list_tools()result.tools 반환.
  • call_toolself.session().call_tool(tool_name, tool_input) 반환.
  • uv run mcp_client.py로 도구 정의 확인, uv run main.py로 Claude가 read_document 실행.

정리 & 점검

Recap & check

Q1MCPClient 클래스가 감싸는 것은 무엇인가요?

Q2list_tools는 무엇을 반환하나요?

Q3구현 후 uv run main.py로 "report.pdf 내용?"을 물으면?