0

MCP

팔로우

MCP Client 중심으로 MCP 구조 파악하기 (LLM 빼고)

AF 김태영
2025.04.02 08:17
1214

MCP를 단 번에 이해하기란 쉽지 않습니다. LLM, Function Calling, Server-Client, Protocol, 로컬 호스트(claude, cursor)… 게다가 다양한 프로그래밍 언어까지 뒤엉켜 하나의 생태계를 이루고 있습니다. 그냥 쓰기만 한다면 개념만 알아도 되지만, 직접 시스템을 구축하려면 핵심 요소들에 대한 이해는 필수입니다. 그래서! 이번엔 MCP Host나 LLM은 모두 걷어내고, MCP Client에 집중해서 테스트해봤습니다. Claude가 제공하는 파일 시스템 기반 MCP Server와 통신하면서, 파이썬으로 직접 호출한 소스코드도 공유합니다. MCP 공부 중에 모듈별로 뜯어보고 싶은 분들에게 작은 실마리가 되길 바랍니다.

목차

  1. MCP란 무엇인가?
  2. 프로젝트 구조 및 준비
  3. mcp_server_config.json으로 서버 파라미터 관리
  4. 클라이언트 코드 구현
  5. 잠깐 Stdio이란?
  6. 실행 및 예시 출력
  7. 정리

1. MCP란 무엇인가?

Model Context Protocol(MCP)는 LLM(예: Claude, GPT)과 다양한 툴(데이터베이스, 파일시스템, 기타 API 등)을 연결하는 클라이언트-서버 프로토콜입니다.

  • 서버(MCP Server): 특정한 기능(‘도구’)을 제공하고, MCP 메시지를 받아 처리합니다. 예: 파일시스템 접근, 데이터 쿼리, HTTP 호출 등.
  • 클라이언트(MCP Client): MCP 서버에 접속하여 필요한 작업을 요청합니다.

이 프로토콜은 JSON-RPC 2.0 스타일로 요청(Request)·응답(Response)·알림(Notifications)을 주고받아 확장성과 호환성이 높습니다.

2. 프로젝트 구조 및 준비

예시 디렉토리 구조는 다음과 같습니다:

mcp-client/
 ┣ .venv/                   # 가상환경(uv venv)
 ┣ mcp_server_config.json   # MCP 서버 설정 파일(예: filesystem 서버)
 ┣ client.py                # MCP 클라이언트 코드
 ┗ ...

가상환경 설치 예시 (uv CLI)

아래와 같이 프로젝트 폴더를 만듭니다.

uv init mcp-client
cd mcp-client
uv venv
source .venv/bin/activate  # (Windows: .venv\Scripts\activate)
uv add mcp

이제 프로젝트 폴더 내에서 client.pymcp_server_config.json을 작성해볼 수 있습니다.

3. mcp_server_config.json으로 서버 파라미터 관리

이 예시에서는 파일시스템 접근을 클로드에서 제공해주는 파일시스템 MCP 서버를 사용합니다. 서버를 실행하기 위한 명령어파라미터를 JSON 파일로 관리할 수 있습니다. 아래 내용으로 “mcp_server_config.json”을 저장합니다.

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "."
      ]
    }
  }
}
  • mcpServers: MCP 서버 목록을 JSON으로 정의합니다. 여기서는 filesystem이라는 이름의 서버 한 개를 사용합니다.
  • command: MCP 서버를 실제로 실행할 명령어(npx, python 등).
  • args: 명령어에 전달될 인자(매개변수) 목록. 위 예시에서는 -y 옵션과 함께 @modelcontextprotocol/server-filesystem 패키지를 실행하고, 현재 디렉토리(.)를 루트로 삼아서 MCP 파일시스템 서버를 띄웁니다.

이 설정 파일을 사용하면, Python 클라이언트 코드에서 쉽게 서버 정보를 읽어와서 서버를 실행할 수 있습니다.

4. 클라이언트 코드 구현

MCP 서버(filesystem)에 연결하여, 서버가 제공하는 도구(tools) 목록을 조회한 뒤, 디렉토리를 나열하고, 특정 확장자를 가진 텍스트 파일만 읽어 일부 내용을 출력하는 로직을 담고 있습니다. 주요 단계를 조금씩 나누어 설명합니다.

4.1 필요한 모듈 임포트

필요한 모듈을 임포트 합니다.

import json, os, asyncio
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
  • json: mcp_server_config.json을 읽고 파싱하기 위해 사용.
  • os: 파일 경로를 합치거나(os.path.join) 운영체제별 경로 처리를 위해 사용.
  • asyncio: MCP 클라이언트가 비동기 방식으로 동작하므로(async/await) 필요.
  • contextlib.AsyncExitStack: 비동기 컨텍스트(async with)를 여러 겹으로 깔끔하게 관리하기 위해 사용.
  • mcp 관련 임포트:
    • ClientSession: MCP 서버와의 세션을 나타내는 객체.
    • StdioServerParameters: MCP 서버 실행 정보를 담는 파라미터 클래스.
    • stdio_client: 표준 입출력을 통해 서버와 연결하는 함수.

4.2 main() 함수 전반부

서버 설정을 준비하는 코드입니다.

async def main():
    # 1. 설정 파일 읽기 및 서버 연결 준비
    with open("mcp_server_config.json") as f:
        config = json.load(f)["mcpServers"]["filesystem"]
    
    server_params = StdioServerParameters(
        command=config["command"], 
        args=config["args"], 
        env=None
    )
  • 설정 파일 읽기
    • mcp_server_config.json을 열어서 JSON 파싱 후, "mcpServers" 하위 "filesystem" 키의 설정 정보를 config 변수에 저장합니다.
  • 서버 실행 파라미터 설정
    • StdioServerParameterscommandargs를 지정합니다.
    • envNone으로 두면, 현재 환경 변수를 그대로 사용합니다. 필요하다면 환경 변수를 추가할 수도 있습니다.

4.3 서버 실행 및 MCP 세션 초기화

서버 실행을 client에서 할 수 있습니다.

    # 2. 서버 연결 및 세션 설정
    stack = AsyncExitStack()
    async with stack:
        # 서버 프로세스 시작 및 세션 초기화
        stdio, write = await stack.enter_async_context(stdio_client(server_params))
        session = await stack.enter_async_context(ClientSession(stdio, write))
        await session.initialize()
  • AsyncExitStack
    • stack = AsyncExitStack()를 통해, 비동기 컨텍스트를 안전하게 열고 닫을 수 있는 스택을 생성합니다.
    • async with stack: 구문에 들어가면, 이 블록 내에서 enter_async_context로 여러 개의 컨텍스트를 등록한 뒤, 블록이 끝날 때 한꺼번에 정리합니다.
  • stdio_client(...)
    • stdio_client(server_params)는 MCP 서버를 subprocess로 실행한 뒤, 그 프로세스의 표준 입출력과 연결합니다.
    • 이 반환값 stdio, write는 표준 입력/출력을 비동기로 주고받기 위한 스트림 객체입니다.
  • ClientSession
    • ClientSession(stdio, write)를 통해, MCP 프로토콜을 구현한 세션을 생성합니다.
    • session.initialize()는 MCP 서버와 최초 Handshake 과정을 거쳐서 세션을 사용 가능한 상태로 만듭니다.

4.4 MCP 도구 목록 조회 및 출력

MCP Server의 도구들을 조회할 수 있습니다.

        # 3. 도구 목록 조회 및 출력
        tools_response = await session.list_tools()
        tool_names = [tool.name for tool in tools_response.tools]
        print("도구:", ", ".join(tool_names)) 
  • session.list_tools()를 호출하면, MCP 서버가 제공하는 “도구”(tool) 리스트를 얻을 수 있습니다.
  • 예: ["list_allowed_directories", "list_directory", "read_file", ...]
  • 이 목록을 출력해보면, MCP 서버가 어떤 기능들을 노출하고 있는지 한눈에 파악할 수 있습니다.

4.5 허용된 디렉토리 목록 조회

json에 설정된 디렉토리 목록을 조회할 수 있습니다.

        # 4. 허용된 디렉토리 목록 조회
        allowed_response = await session.call_tool("list_allowed_directories")
        allowed_text = allowed_response.content[0].text
        
        # 디렉토리 목록 추출
        directories = []
        for line in allowed_text.split('\n'):
            if line.strip() and "Allowed" not in line:
                directories.append(line.strip())
        
        if not directories:
            directories = ['.']
            
        print(f"디렉토리: {', '.join(directories)}")
  • call_tool(tool_name, parameters=None)
    • 해당 MCP 서버 도구(tool_name)를 호출합니다.
    • list_allowed_directories는 파일시스템 서버가 “접근 허용 디렉토리” 목록을 반환하는 기능입니다.
    • allowed_response.content[0].text는 응답 본문을 텍스트 형태로 가져옵니다.
  • 디렉토리 목록 파싱
    • 텍스트 응답은 줄 단위로 되어 있으므로 split('\n')로 잘라서 각 줄을 확인합니다.
    • “Allowed” 같은 안내 문구는 제외하고, 실제 경로만 추출해 directories 리스트에 담습니다.
    • 만약 비어 있으면(if not directories:), 기본적으로 현재 디렉토리(".")만 포함합니다.

4.6 텍스트 파일 확장자 정의

읽고자 하는 파일 타입을 정의합니다.

        # 5. 텍스트 파일 확장자 정의
        extensions = ['.txt', '.md', '.py', '.json', '.csv', '.log', '.html', '.css', '.js']
  • 이 예시에서는 텍스트로 볼 만한 확장자를 지정합니다.
  • 여기 포함된 파일만 대상으로 실제 파일 내용을 확인하거나 출력합니다.

4.7 디렉토리 순회 및 파일 내용 출력

허용된 디렉토리 내에서 파일 목록을 가지고 와서 하나씩 읽습니다.

         # 6. 각 디렉토리 처리
        for directory in directories:
            print(f"\n--- {directory} ---")
            
            # 디렉토리 내용 조회 및 파일 필터링
            dir_response = await session.call_tool("list_directory", {"path": directory})
            dir_text = dir_response.content[0].text
            
            # 텍스트 파일 찾기
            text_files = []
            for line in dir_text.split('\n'):
                if line.startswith('[FILE]'):
                    filename = line.replace('[FILE]', '').strip()
                    # 텍스트 파일 확장자 확인
                    if any(filename.lower().endswith(ext) for ext in extensions):
                        text_files.append(filename)
            
            # 파일 목록 출력
            if not text_files:
                print("파일 없음")
                continue
                
            print(f"{len(text_files)}개: {', '.join(text_files[:3])}" + 
                 ("..." if len(text_files) > 3 else ""))
            
            # 파일 내용 출력 (최대 2개 파일)
            for filename in text_files[:2]:
                try:
                    # 파일 내용 읽기 및 처음 3줄 출력
                    file_path = os.path.join(directory, filename)
                    file_response = await session.call_tool("read_file", {"path": file_path})
                    content = file_response.content[0].text
                    lines = content.split('\n')
                    
                    print(f"\n> {filename}:")
                    for i in range(min(3, len(lines))):
                        print(f"  {lines[i]}")
                    
                    if len(lines) > 3:
                        print("  ...")
                except:
                    print(f"오류: {filename} 읽기 실패")
  • 디렉토리 순회
    • 앞서 구한 directories 리스트에 담긴 각 디렉토리를 순회하며 처리합니다.
  • list_directory 호출
    • session.call_tool("list_directory", {"path": directory})로 해당 디렉토리에 있는 파일과 폴더 목록을 얻습니다.
    • 응답 텍스트는 [FILE], [DIR]와 같은 태그로 구분되어 있을 수 있습니다.
  • 텍스트 파일 필터링
    • 줄마다 “[FILE]”로 시작하는 항목만 추출하고, 지정한 확장자 리스트(extensions)에 맞는 파일만 text_files에 담습니다.
  • 파일 내용 읽기
    • read_file 도구를 이용해 실제 파일 내용을 가져옵니다.
    • 가져온 내용(file_response.content[0].text)을 3줄만 출력하되, 줄 수가 더 많으면 “...”을 덧붙여 표시합니다.

4.8 메인 함수 실행부

메인 함수를 실행합니다.

if __name__ == "__main__":
    asyncio.run(main()) 
  • Python 스크립트가 직접 실행될 경우 main() 코루틴을 asyncio.run(...)으로 구동합니다.
  • 여기서부터 비동기로 MCP 서버와 통신이 이뤄집니다.

4.9 최종 전체 코드

위 내용을 합치면 client.py는 아래와 같이 작성할 수 있습니다.

import json, os, asyncio
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    # 1. 설정 파일 읽기 및 서버 연결 준비
    with open("mcp_server_config.json") as f:
        config = json.load(f)["mcpServers"]["filesystem"]
    
    server_params = StdioServerParameters(
        command=config["command"], 
        args=config["args"], 
        env=None
    )
    
    # 2. 서버 연결 및 세션 설정
    stack = AsyncExitStack()
    async with stack:
        # 서버 프로세스 시작 및 세션 초기화
        stdio, write = await stack.enter_async_context(stdio_client(server_params))
        session = await stack.enter_async_context(ClientSession(stdio, write))
        await session.initialize()
        
        # 3. 도구 목록 조회 및 출력
        tools_response = await session.list_tools()
        tool_names = [tool.name for tool in tools_response.tools]
        print("도구:", ", ".join(tool_names))
        
        # 4. 허용된 디렉토리 목록 조회
        allowed_response = await session.call_tool("list_allowed_directories")
        allowed_text = allowed_response.content[0].text
        
        # 디렉토리 목록 추출
        directories = []
        for line in allowed_text.split('\n'):
            if line.strip() and "Allowed" not in line:
                directories.append(line.strip())
        
        if not directories:
            directories = ['.']
            
        print(f"디렉토리: {', '.join(directories)}")
        
        # 5. 텍스트 파일 확장자 정의
        extensions = ['.txt', '.md', '.py', '.json', '.csv', '.log', '.html', '.css', '.js']
        
        # 6. 각 디렉토리 처리
        for directory in directories:
            print(f"\n--- {directory} ---")
            
            # 디렉토리 내용 조회 및 파일 필터링
            dir_response = await session.call_tool("list_directory", {"path": directory})
            dir_text = dir_response.content[0].text
            
            # 텍스트 파일 찾기
            text_files = []
            for line in dir_text.split('\n'):
                if line.startswith('[FILE]'):
                    filename = line.replace('[FILE]', '').strip()
                    # 텍스트 파일 확장자 확인
                    if any(filename.lower().endswith(ext) for ext in extensions):
                        text_files.append(filename)
            
            # 파일 목록 출력
            if not text_files:
                print("파일 없음")
                continue
                
            print(f"{len(text_files)}개: {', '.join(text_files[:3])}" + 
                 ("..." if len(text_files) > 3 else ""))
            
            # 파일 내용 출력 (최대 2개 파일)
            for filename in text_files[:2]:
                try:
                    # 파일 내용 읽기 및 처음 3줄 출력
                    file_path = os.path.join(directory, filename)
                    file_response = await session.call_tool("read_file", {"path": file_path})
                    content = file_response.content[0].text
                    lines = content.split('\n')
                    
                    print(f"\n> {filename}:")
                    for i in range(min(3, len(lines))):
                        print(f"  {lines[i]}")
                    
                    if len(lines) > 3:
                        print("  ...")
                except:
                    print(f"오류: {filename} 읽기 실패")

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

5. 잠깐 Stdio이란?

Stdio는 MCP에서 서버와 클라이언트가 표준 입력/출력을 통해 메시지를 교환하는 방식인 Stdio Transport를 의미합니다. 

  • 장점:
    • 로컬 프로세스 간 통신에 간편
    • 네트워크 설정 없이 바로 테스트 가능
    • 프로세스 시작/종료가 명확해 디버깅에 용이
  • 한계:
    • 원격 서버와 연결이 필요한 경우에는 SSE + HTTP Transport(웹소켓, SSE 등) 활용이 일반적
    • 로컬 환경이 아닌 분산 아키텍처에는 부적합

따라서, 로컬에서 빠르게 MCP 서버-클라이언트 구조를 만들고 테스트하고 싶다면, Stdio가 가장 간단한 선택입니다.

6. 실행 및 예시 출력

1. mcp_server_config.json을 준비합니다.

2. 아래처럼 클라이언트를 실행합니다:

uv run client.py

또는 가상환경에서

python client.py

3. 정상 동작 시, MCP 서버가 함께 실행되며, 콘솔에 “사용 가능한 도구: ...”와 디렉토리/파일 목록 및 일부 파일 내용이 출력됩니다.

예시 출력

사용 가능한 도구: list_allowed_directories, list_directory, read_file
허용 디렉토리: ['.', './data']

=== . 디렉토리 확인 ===
  텍스트 파일 목록(3개): ['README.md', 'test.txt', 'example.py']...
  
> README.md 내용 (상위 3줄):
  # Project Readme
  This is an example README file.
  For demonstration.

> test.txt 내용 (상위 3줄):
  test row1
  test row2
  test row3

7. 정리

이 예시 코드는 Python에서 로컬 파일시스템 MCP 서버에 연결해 텍스트 파일을 자동으로 스캔하고 일부 내용을 출력해 봄으로써, MCP 클라이언트가 어떤 식으로 작동하는지 확인하는 좋은 출발점이 됩니다. 이후에는 자신만의 LLM과 연동한 MCP Client를 만들어서 어떻게 도구를 호출하는 지 살펴보시면 좋을 것 같습니다. 추가 고려사항은 아래와 같습니다.

  • Stdio Transport동일 머신에서 MCP 서버-클라이언트를 빠르게 연결하는 데 최적화되어 있습니다.
  • 원격 서버 또는 웹 기반 연동이 필요하다면, SSE + HTTP Transport를 사용해 HTTPS/TLS, 인증 등을 고려할 수 있습니다.
  • MCP의 프로토콜 계층은 바뀌지 않으므로, Transport 방식을 교체해도 list_tools(), call_tool() 등의 함수 사용법은 동일합니다.
  • LLM과의 연동: 여기서는 LLM을 쓰지 않았지만, 같은 구조에서 ChatGPT/Claude/GPT4 등과 결합해 자연어로 MCP 도구를 호출하는 형태로 확장할 수 있습니다.
     

참조 링크

 

0
0개의 댓글
로그인 후 이용해주세요!