MCP의 호스트·클라이언트·서버 구분하기
Model Context Protocol에서 사용자 애플리케이션과 연결 관리 계층, 도구·리소스 제공자의 책임을 나눠 봅니다.
무료 공개 · 최근 수정
단 12줄로 시작하는 표준 도구 서버
AI 모델에게 외부 날씨 정보를 조회하는 기능을 연동한다고 가정해 보겠습니다. 전통적인 방식이라면 모델 제공자가 요구하는 복잡한 함수 스키마 규격을 손수 작성하고, 각 에이전트 프레임워크마다 서로 다른 접착 코드를 짜야 합니다. 하지만 공식 Python SDK의 MCPServer를 사용하면 일반 파이썬 함수 하나만으로 즉시 동작하는 도구 서버를 구성할 수 있습니다.
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("WeatherService")
@mcp.tool()
def get_weather(city: str) -> str:
"""지정한 도시의 현재 날씨와 기온 요약을 반환합니다."""
# 외부 기상 API 연동을 모사한 안전한 예시 반환값
return f"{city}의 현재 날씨는 맑음이며 기온은 21도입니다."
if __name__ == "__main__":
mcp.run()위 코드는 공식 Python SDK인 mcp 2.3.0 기준(2026-10-03 확인)이며, 이전 1.x 버전에서는 from mcp.server.fastmcp import FastMCP 형식을 사용했습니다. 위 코드를 weather_server.py 파일로 저장하고 직접 실행하면 화면에는 아무것도 출력되지 않고 표준 입력으로 들어오는 JSON-RPC 요청을 대기합니다. stdio 전송 환경에서 표준 출력(stdout)은 프로토콜 통신 메시지 전용으로 사용되기 때문입니다. 실제로 사람이 수동으로 명령을 입력하기보다는, 호스트(클라이언트) 애플리케이션이 이 프로세스를 자식 프로세스로 직접 띄워 연결을 맺고 통신합니다.
별도의 복잡한 스키마 문서를 손수 작성하지 않았지만, 프레임워크는 함수의 타입 힌트(city: str)와 독스트링을 분석해 도구의 이름, 설명, 입력 스키마와 출력 스키마를 표준 JSON-RPC 형식으로 자동 노출합니다. stdio 전송 환경에서는 별도의 네트워크 포트를 열지 않고도 부모 프로세스와 표준 입출력 파이프를 통해 고속으로 데이터를 주고받으므로, 로컬 환경에서 안전하고 가볍게 구동할 수 있습니다. 이 간단한 스크립트가 데스크톱 애플리케이션, AI 코드 에디터, 독자적인 사내 에이전트 등 수많은 도구 소비자에게 즉시 인식될 수 있는 기반이 바로 Model Context Protocol(MCP)입니다.
호스트·클라이언트·서버가 분담하는 경계
위에서 작성한 파이썬 스크립트는 단독으로 동작하는 완성형 프로그램이 아닙니다. 프로토콜 생태계는 크게 세 가지 독립된 참여자로 나뉘어 상호작용합니다.
그림: 호스트는 서버마다 별도 클라이언트와 전송 경로를 관리합니다.
사용자와 직접 대화하며 LLM을 호출하는 최상위 애플리케이션을 MCP 호스트(Host)라고 부릅니다. 호스트는 사용자의 프롬프트를 처리하고, 모델이 도구 사용을 요청했을 때 실행 여부를 사용자에게 묻고 검증하는 최종 보안 통제소 역할을 맡습니다. 단일 호스트는 필요에 따라 로컬 파일 시스템 서버, 데이터베이스 연동 서버, 이슈 추적기 서버 등 여러 개의 독립된 서버에 동시에 연결될 수 있습니다.
호스트 내부에서 동작하며 개별 서버와의 통신을 1:1로 전담하는 계층은 MCP 클라이언트(Client)입니다. 클라이언트는 서버 프로세스를 자식 프로세스로 띄우거나 원격 네트워크 소켓을 열어 프로토콜 규약에 맞게 통신 채널을 유지합니다. 서버 하나에서 네트워크 장애가 발생하거나 응답 지연이 생겨도 해당 클라이언트 수준에서 타임아웃을 격리함으로써 호스트 전체 프로세스가 멈추는 사태를 방지합니다.
마지막으로 실제 데이터나 시스템 기능을 외부로 노출하는 주체가 MCP 서버(Server)입니다. 앞서 작성한 날씨 서버처럼, 서버는 도구(Tools), 데이터 리소스(Resources), 프롬프트 템플릿(Prompts)을 규격에 맞춰 클라이언트에 제공합니다. 서버가 많은 기능을 가지고 있더라도 호스트가 명시적으로 승인하지 않은 작업은 임의로 실행될 수 없습니다.
세 참여자의 명확한 역할 분담을 정리하면 다음과 같습니다.
| 참여 계층 | 핵심 역할 | 직접 처리하는 작업 | 다루지 않는 범위 |
|---|---|---|---|
| MCP 호스트 | 사용자 인터페이스 제공 및 전체 실행 정책 통제 | 모델 추론 제어, 사람의 개입(Human-in-the-Loop) 승인 UI, 다중 서버 클라이언트 조율 | 개별 도구의 내부 로직 실행, 저수준 바이트 스트림 파싱 |
| MCP 클라이언트 | 특정 서버와의 1:1 프로토콜 연결 관리 | 세션 초기화 및 기능 협상, JSON-RPC 메시지 직렬화 및 역직렬화, 전송 오류 복구 | 최종 프롬프트 구성, 사용자 실행 권한 판정, 도구 결과의 타당성 검증 |
| MCP 서버 | 고유 기능 및 데이터의 표준 인터페이스 제공 | 도구 목록 노출, 수신 인자 유효성 검증, 비즈니스 로직 실행 및 결과 반환 | 호스트의 전역 보안 정책, 어떤 도구를 언제 호출할지에 대한 모델 추론 |
와이어를 오가는 JSON-RPC 메시지 읽기
클라이언트와 서버가 표준 입출력이나 Streamable HTTP 파이프라인 위에서 통신할 때는 JSON-RPC 2.0 명세에 기반한 구조화된 메시지를 교환합니다.
연결이 맺어지면 양측은 먼저 initialize 요청과 응답을 통해 프로토콜 버전과 지원 가능한 기능(Capabilities)을 확인하는 초기화 과정을 거칩니다. 핸드셰이크가 완료된 직후, 클라이언트는 서버가 어떤 도구를 가지고 있는지 파악하기 위해 tools/list 요청을 전송합니다.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}이에 대해 서버는 자신이 등록받은 도구 목록과 인자 스키마를 담은 응답을 반환합니다.
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"description": "지정한 도시의 현재 날씨와 기온 요약을 반환합니다.",
"inputSchema": {
"properties": {
"city": {
"title": "City",
"type": "string"
}
},
"required": [
"city"
],
"type": "object",
"title": "get_weatherArguments"
},
"name": "get_weather",
"outputSchema": {
"properties": {
"result": {
"title": "Result",
"type": "string"
}
},
"required": [
"result"
],
"type": "object",
"title": "get_weatherOutput"
}
}
]
}
}이후 모델이 사용자의 질문을 해결하기 위해 도구 호출을 결정하면, 클라이언트는 tools/call 메서드를 전송합니다.
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"city": "서울"
}
}
}서버는 내부 파이썬 함수를 실행하고 그 결과를 표준 형식으로 감싸 클라이언트에 회신합니다.
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "서울의 현재 날씨는 맑음이며 기온은 21도입니다."
}
],
"structuredContent": {
"result": "서울의 현재 날씨는 맑음이며 기온은 21도입니다."
},
"isError": false
}
}이 메시지들의 구조를 한 줄씩 살펴보면 다음과 같은 명세 필드로 이루어져 있습니다.
jsonrpc: 통신에 사용 중인 JSON-RPC 표준 규격 버전("2.0")을 고정 명시합니다.id: 비동기 다중 통신 환경에서 보낸 요청과 돌아온 응답을 1:1로 결합하기 위한 고유 식별 번호입니다.method: 클라이언트가 서버에 실행을 요청하는 원격 프로토콜 메서드 이름입니다. 도구 목록 조회는tools/list, 실제 실행은tools/call을 사용합니다.params: 메서드 처리에 필요한 매개변수 묶음입니다.tools/call에서는 호출할 도구의name과 모델이 생성한 인자 객체arguments를 포함합니다.result: 서버의 요청 처리가 성공했을 때 호출 결과를 실어 보내는 최상위 컨테이너 객체입니다.inputSchema: 함수의 매개변수 타입 힌트로부터 자동 도출된 인자 유효성 검증 규칙으로,title에get_weatherArguments가 지정됩니다.outputSchema: 함수의 반환 타입(str)을 기반으로 자동 생성된 출력 스키마로,title에get_weatherOutput이 지정되어 클라이언트가 결과 데이터 형식을 사전에 파악할 수 있도록 돕습니다.content: 도구 실행 결과를 나타내는 콘텐츠 배열입니다. 사람이 읽을 수 있는 텍스트(type: "text") 외에도 이미지나 바이너리 데이터를 포함할 수 있습니다.structuredContent: 함수 반환값을 구조화된 객체 형태로 함께 제공하여, 클라이언트 프로그램이 문자열을 재파싱하지 않고도result키로 즉시 참조할 수 있도록 합니다.isError: 함수 로직 실행 중 예외나 비즈니스 오류가 발생했는지를 나타내는 불리언 값입니다. 네트워크 전송 레벨의 JSON-RPC 에러와 별개로 도구 자체의 실패를 호스트에 전달합니다.
도구를 MCP 서버로 분리하는 세 가지 판단 기준
에이전트 애플리케이션을 개발할 때 모든 도구를 무조건 MCP 서버로 분리할 필요는 없습니다. 단순한 사칙연산이나 특정 에이전트 전용 문자열 가공 로직을 외부 프로세스 서버로 떼어내면 오히려 프로세스 간 통신 오버헤드와 운영 복잡도만 증가합니다.
도구를 애플리케이션 내부 함수로 둘지, 독립된 MCP 서버로 분리할지는 다음 세 가지 기준에 따라 판단합니다.
첫째, 재사용 범위입니다. 개발 중인 도구가 단일 에이전트의 내부 작업에만 국한된다면 인프로세스(In-process) 함수로 충분합니다. 하지만 사내 데이터베이스 조회, Git 저장소 제어, 사내 지식 베이스 검색처럼 데스크톱 애플리케이션, 개발 보조 CLI, 백엔드 배치 워크플로 등 여러 클라이언트가 동시에 소비해야 하는 공통 기능이라면 MCP 서버로 분리하는 것이 중복 개발을 없애는 최선의 선택입니다.
둘째, 보안 및 권한 격리입니다. 도구가 운영체제 셸 명령을 실행하거나 중요 파일 시스템에 쓰기 작업을 수행해야 한다면 에이전트 프로세스와 동일한 권한을 주어서는 안 됩니다. MCP 서버로 분리하면 해당 도구 프로세스에만 제한된 디렉터리 마운트나 최소 권한(Principle of Least Privilege)을 부여하고, 호스트 수준에서 사용자의 명시적 승인 창을 끼워 넣는 보안 샌드박스를 구성하기 수월해집니다.
셋째, 배포 및 수명주기 독립성입니다. 무거운 의존성 라이브러리를 요구하는 도구를 에이전트 코드베이스에 직접 포함하면 빌드 용량이 커지고 패키지 충돌 위험이 따릅니다. 도구를 독립 컨테이너 기반의 MCP 서버로 격리하면 에이전트 로직과 도구 환경을 서로 다른 일정으로 배포하고 독립적으로 수평 확장할 수 있습니다.
ReAct와 도구 호출에서 다루는 모델의 추론-행동(Thought-Action-Observation) 반복 루프에서, MCP는 추론을 담당하는 브레인과 실제 환경을 변경하는 손발 사이를 깔끔하게 분리해 주는 표준 계약선이 됩니다. 모델이 생성한 도구 인자는 클라이언트와 프로토콜 계층을 거치며 정형 검증을 받고, 관리자가 지정한 정책에 따라 위험 작업은 사람의 승인 단계로 안전하게 우회됩니다.
참고 문서
설명이 어렵거나 잘못된 부분을 발견하셨나요?
문서 수정 의견 보내기