CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API
MCP
Implementing a client
이제 mcp_client.py의 MCPClient 클래스를 봅니다. 이 클래스는 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분MCPClient = ClientSession(실제 연결)을 감싸는 클래스
connect·cleanup·async enter/exit에 정리 코드가 이미 있다
클라이언트는 서버 기능을 코드베이스에 노출한다
list_tools → self.session().list_tools() → result.tools
call_tool → self.session().call_tool(name, input)
uv run mcp_client.py로 도구 정의 출력 → main.py로 Claude 호출
with 블록. mcp_client.py를 직접 실행해 서버에 붙어 동작을 확인.서버가 안정됐으니 이제 클라이언트 차례입니다. mcp_client.py의 MCPClient는 클래스 하나로, SDK가 주는 ClientSession(서버와의 실제 연결)을 감쌉니다. 코드가 큰 이유는 세션의 정리(cleanup) 때문 — 그 코드는 이미 들어 있습니다.
세션은 닫을 때 리소스 정리가 필요합니다. ClientSession을 직접 쓰기보다 더 큰 클래스로 감싸 connect·async enter/exit에서 정리를 맡기는 게 흔한 방식입니다. 또 학습용이라 이 프로젝트는 서버·클라이언트를 둘 다 만들지만, 실무에선 보통 한쪽만 만듭니다.
클라이언트는 서버 기능을 코드베이스에 노출합니다. 이번엔 두 함수만 구현합니다. list_tools는 세션에 도구 목록을 요청해 result.tools를 돌려주고, call_tool은 이름·입력 인자로 도구를 실행합니다.
async def list_tools(self) -> list[types.Tool]: result = await self.session().list_tools() return result.tools
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가 도구 실행을 요청했을 때 호출됩니다 — 강조된 단계가 그 지점입니다.
파일 하단의 with 블록에서 mcp_client.py를 직접 실행하면 서버에 붙어 동작을 확인할 수 있습니다. list_tools를 호출해 도구 정의를 출력해 봅니다.
# 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
[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'})]각 도구가 description과 input schema를 갖고 있습니다 — 이게 곧 Claude에 넘어갈 도구 정의입니다. 이제 CLI를 돌려 Claude가 이 도구를 실제로 쓰게 합니다.
$ uv run main.py > report.pdf 문서 내용이 뭐야? report.pdf 문서의 내용을 가져왔습니다. 이 보고서는 20미터 응축탑의 상태에 관한 내용입니다 ...
ClientSession(실제 연결)을 감싸 정리를 맡고 서버 기능을 노출한다.list_tools → self.session().list_tools() → result.tools 반환.call_tool → self.session().call_tool(tool_name, tool_input) 반환.uv run mcp_client.py로 도구 정의 확인, uv run main.py로 Claude가 read_document 실행.Q1MCPClient 클래스가 감싸는 것은 무엇인가요?
세션은 정리가 필요해서, 직접 쓰기보다 MCPClient로 감싸 connect·async enter/exit에서 cleanup을 맡깁니다.
Q2list_tools는 무엇을 반환하나요?
세션에 도구 목록을 요청하고 result에서 tools만 돌려줍니다 — Claude에 넘길 도구 정의입니다.
Q3구현 후 uv run main.py로 "report.pdf 내용?"을 물으면?
요청과 함께 도구 목록이 전송되고, Claude가 read_document를 골라 call_tool로 실행해 내용을 가져옵니다.
클라이언트가 도구를 나열·실행할 수 있게 됐습니다. 다음은 서버가 데이터를 노출하는 또 다른 방법, 리소스를 정의합니다 — @ 멘션으로 문서를 끌어오는 기능입니다. → 리소스 정의하기
전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.
이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.