본문으로 건너뛰기

MCP 클라이언트 구축

이 튜토리얼에서는 MCP 서버에 연결되는 LLM 기반 챗봇 클라이언트를 구축하는 방법을 알아봅니다.

시작하기 전에 MCP 서버 구축 튜토리얼을 먼저 살펴보면 클라이언트와 서버가 통신하는 방식을 이해하는 데 도움이 됩니다.

이 튜토리얼의 전체 코드는 여기에서 확인할 수 있습니다.

시스템 요구 사항

시작하기 전에 시스템이 다음 요구 사항을 충족하는지 확인하세요.

  • Mac 또는 Windows 컴퓨터
  • 최신 버전의 Python 설치
  • 최신 버전의 uv 설치
  • Python MCP SDK 2.0.0 이상 사용

환경 설정

먼저 uv로 새 Python 프로젝트를 만듭니다.

# Create project directory
uv init mcp-client
cd mcp-client

# Create virtual environment
uv venv

# Activate virtual environment
source .venv/bin/activate

# Install required packages
uv add mcp anthropic python-dotenv

# Remove boilerplate files
rm main.py

# Create our main file
touch client.py
# Create project directory
uv init mcp-client
cd mcp-client

# Create virtual environment
uv venv

# Activate virtual environment
.venv\Scripts\activate

# Install required packages
uv add mcp anthropic python-dotenv

# Remove boilerplate files
del main.py

# Create our main file
new-item client.py

API 키 설정

Anthropic Console에서 발급한 Anthropic API 키가 필요합니다.

키를 저장할 .env 파일을 만듭니다.

echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env

.gitignore.env를 추가합니다.

echo ".env" >> .gitignore

클라이언트 생성

가져오기 및 기본 설정

먼저 필요한 항목을 가져오고 파일 전체에서 공유할 요소를 설정합니다.

import asyncio
import sys

from mcp import Client, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp_types import TextContent

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv() # load environment variables from .env

MODEL = "claude-opus-5"
anthropic = Anthropic()

Client는 프로그램이 서버와 통신할 때 사용하는 단일 객체입니다. 도구 목록 조회, 도구 호출, 리소스 읽기는 모두 이 객체의 메서드로 제공됩니다.

서버 연결 관리

다음으로 주어진 서버 스크립트에 따라 실행할 프로세스를 결정합니다.

def server_params(server_script_path: str) -> StdioServerParameters:
"""Describe the subprocess that runs an MCP server

Args:
server_script_path: Path to the server script (.py or .js)
"""
if server_script_path.endswith(".py"):
command = "python"
elif server_script_path.endswith(".js"):
command = "node"
else:
raise ValueError("Server script must be a .py or .js file")

return StdioServerParameters(command=command, args=[server_script_path])

StdioServerParameters는 연결 자체가 아니라 연결 설정입니다. stdio_client()가 이 설정을 stdio 전송 방식으로 변환하고, Clientasync with 블록에 진입하면 해당 전송 연결이 열립니다. 이 두 작업은 main()에서 처리합니다.

쿼리 처리 로직

이제 쿼리를 처리하고 도구 호출을 다루는 핵심 기능을 추가합니다.

async def process_query(client: Client, query: str) -> str:
"""Process a query using Claude and available tools"""
messages = [
{
"role": "user",
"content": query
}
]

tool_list = await client.list_tools()
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.input_schema
} for tool in tool_list.tools]

# Initial Claude API call
response = anthropic.messages.create(
model=MODEL,
max_tokens=1000,
messages=messages,
tools=available_tools
)

# Process response and handle tool calls
final_text = []
tool_results = []

for content in response.content:
if content.type == 'text':
final_text.append(content.text)
elif content.type == 'tool_use':
tool_name = content.name
tool_args = content.input

# Execute tool call
result = await client.call_tool(tool_name, tool_args)
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")

tool_results.append({
"type": "tool_result",
"tool_use_id": content.id,
"content": "\n".join(
block.text
for block in result.content
if isinstance(block, TextContent)
),
"is_error": result.is_error
})

if tool_results:
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})

# Get next response from Claude
response = anthropic.messages.create(
model=MODEL,
max_tokens=1000,
messages=messages,
tools=available_tools
)

for content in response.content:
if content.type == 'text':
final_text.append(content.text)

return "\n".join(final_text)

call_toolCallToolResult를 반환합니다. content는 블록 목록이므로 .text를 읽기 전에 TextContent로 범위를 좁힙니다. 도구 내부에서 예외가 발생해도 여기에서 예외를 다시 던지지는 않습니다. 대신 is_error가 설정된 응답을 반환하며, 이 플래그를 그대로 전달하면 Claude가 오류 메시지를 읽고 다른 방법을 시도할 수 있습니다.

대화형 채팅 인터페이스

이제 채팅 루프를 추가합니다.

async def chat_loop(client: Client) -> None:
"""Run an interactive chat loop"""
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")

while True:
try:
query = (await asyncio.to_thread(input, "\nQuery: ")).strip()
except EOFError:
break

if query.lower() == 'quit':
break

try:
response = await process_query(client, query)
print("\n" + response)
except Exception as e:
print(f"\nError: {e}")

input()은 블로킹 함수이므로 작업자 스레드에서 실행합니다. 그러면 사용자가 입력하는 동안 이벤트 루프가 연결을 계속 처리할 수 있습니다.

기본 진입점

마지막으로 기본 실행 로직을 추가합니다.

async def main() -> None:
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)

async with Client(stdio_client(server_params(sys.argv[1]))) as client:
tool_list = await client.list_tools()
tool_names = [tool.name for tool in tool_list.tools]
print("\nConnected to server with tools:", tool_names)

await chat_loop(client)


if __name__ == "__main__":
asyncio.run(main())

async with 블록이 전체 연결 수명 주기를 담당합니다. 블록에 진입하면 서버를 실행하고 서버와 사용할 프로토콜 버전을 합의하며, 블록을 벗어나면 연결을 끊고 하위 프로세스를 종료합니다. 직접 닫아야 할 항목은 없습니다.

전체 client.py 파일은 여기에서 확인할 수 있습니다.

주요 구성 요소 설명

1. 클라이언트 초기화

  • 하나의 Client가 연결을 담당하며 async with가 전체 수명 주기를 관리합니다.
  • 별도로 호출할 연결/종료 메서드 쌍이 없으며 이후 정리할 항목도 없습니다.
  • Claude와 상호작용하도록 Anthropic 클라이언트를 구성합니다.

2. 서버 연결

  • Python 및 Node.js 서버를 모두 지원합니다.
  • 서버 스크립트 유형을 검증합니다.
  • 서버를 하위 프로세스로 실행하고 stdio로 통신합니다.
  • 연결이 열리면 사용 가능한 도구 목록을 조회합니다.

3. 쿼리 처리

  • 대화 컨텍스트를 유지합니다.
  • Claude의 응답과 도구 호출을 처리합니다.
  • Claude와 도구 사이의 메시지 흐름을 관리합니다.
  • 결과를 일관된 응답으로 결합합니다.

4. 대화형 인터페이스

  • 간단한 명령줄 인터페이스를 제공합니다.
  • 사용자 입력을 처리하고 응답을 표시합니다.
  • 기본적인 오류 처리를 포함합니다.
  • 정상적으로 종료할 수 있습니다.

5. 리소스 관리

  • async with 블록을 벗어나면 연결을 끊고 서버 하위 프로세스를 종료합니다.
  • 쿼리가 실패해도 세션을 종료하지 않고 오류를 보고합니다.
  • quit를 입력하거나 표준 입력을 닫으면 정상적으로 종료됩니다.

자주 사용되는 사용자 지정 지점

  1. 도구 처리

    • 특정 도구 유형을 처리하도록 process_query()를 수정합니다.
    • 도구 호출에 사용자 지정 오류 처리를 추가합니다.
    • 도구별 응답 형식을 구현합니다.
  2. 응답 처리

    • 도구 결과의 형식을 사용자 지정합니다.
    • 응답 필터링 또는 변환을 추가합니다.
    • 사용자 지정 로깅을 구현합니다.
  3. 사용자 인터페이스

    • GUI 또는 웹 인터페이스를 추가합니다.
    • 풍부한 콘솔 출력을 구현합니다.
    • 명령 기록 또는 자동 완성을 추가합니다.

클라이언트 실행

원하는 MCP 서버와 함께 클라이언트를 실행하려면 다음 명령을 사용합니다.

uv run client.py path/to/server.py # python server
uv run client.py path/to/build/index.js # node server

클라이언트는 다음과 같이 작동합니다.

  1. 지정된 서버에 연결합니다.
  2. 사용 가능한 도구 목록을 조회합니다.
  3. 다음 작업을 수행할 수 있는 대화형 채팅 세션을 시작합니다.
    • 쿼리 입력
    • 도구 실행 확인
    • Claude의 응답 수신

서버 빠른 시작의 날씨 서버에 연결했을 때 화면은 다음과 같습니다.

작동 방식

쿼리를 제출하면 다음 과정이 진행됩니다.

  1. 클라이언트가 서버에서 사용 가능한 도구 목록을 가져옵니다.
  2. 쿼리가 도구 설명과 함께 Claude로 전송됩니다.
  3. Claude가 사용할 도구가 있는지 판단하고 해당 도구를 선택합니다.
  4. 클라이언트가 요청된 도구 호출을 서버를 통해 실행합니다.
  5. 결과가 Claude로 다시 전송됩니다.
  6. Claude가 자연어 응답을 생성합니다.
  7. 사용자에게 응답이 표시됩니다.

모범 사례

  1. 오류 처리

    • 실패한 도구가 예외를 던질 것으로 예상하지 말고 result.is_error를 확인합니다.
    • 의미 있는 오류 메시지를 제공합니다.
    • 연결 문제를 안정적으로 처리합니다.
  2. 리소스 관리

    • async with 블록이 연결을 관리하도록 합니다.
    • 서버가 필요한 동안 연결을 열어 둡니다.
    • 서버 연결 해제를 처리합니다.
  3. 보안

    • API 키를 .env에 안전하게 저장합니다.
    • 서버 응답을 검증합니다.
    • 도구 권한을 신중하게 설정합니다.
  4. 도구 이름

    • 도구 이름은 여기에 명시된 형식에 따라 검증할 수 있습니다.
    • 도구 이름이 명시된 형식을 따르면 MCP 클라이언트의 검증을 통과해야 합니다.

문제 해결

서버 경로 문제

  • 서버 스크립트 경로가 올바른지 다시 확인합니다.
  • 상대 경로가 작동하지 않으면 절대 경로를 사용합니다.
  • Windows에서는 경로에 슬래시(/) 또는 이스케이프된 백슬래시(\)를 사용해야 합니다.
  • 서버 파일의 확장자가 올바른지 확인합니다(Python은 .py, Node.js는 .js).

올바른 경로 사용 예시는 다음과 같습니다.

# Relative path
uv run client.py ./server/weather.py

# Absolute path
uv run client.py /Users/username/projects/mcp-server/weather.py

# Windows path (either format works)
uv run client.py C:/projects/mcp-server/weather.py
uv run client.py C:\\projects\\mcp-server\\weather.py

응답 시간

  • 첫 응답에는 최대 30초가 걸릴 수 있습니다.
  • 다음 작업이 진행되는 동안 발생하는 정상적인 현상입니다.
    • 서버 초기화
    • Claude의 쿼리 처리
    • 도구 실행
  • 이후 응답은 일반적으로 더 빠릅니다.
  • 최초 대기 시간에는 프로세스를 중단하지 마세요.

일반적인 오류 메시지

다음 오류가 표시되는 경우:

  • FileNotFoundError: 서버 경로를 확인합니다.
  • Connection refused: 서버가 실행 중이고 경로가 올바른지 확인합니다.
  • Tool execution failed: 도구에 필요한 환경 변수가 설정되어 있는지 확인합니다.
  • Timeout error: Clientread_timeout_seconds 값을 늘리는 방안을 고려합니다.

다음 단계